Compare commits
66 Commits
v2.1.0
...
sgunter/fe
| Author | SHA1 | Date | |
|---|---|---|---|
| bb4fbc3700 | |||
|
|
e8a8b8e1ae | ||
|
|
c21cbd77f0 | ||
| 99a035e29d | |||
|
|
64baa6d290 | ||
|
|
46fe591fa7 | ||
|
|
8f7e1bc0af | ||
|
|
9fc1098ef7 | ||
|
|
ae094416fc | ||
|
|
35d903e7c8 | ||
|
|
2e7dac1e3e | ||
|
|
7bea23a655 | ||
|
|
3d28869e92 | ||
|
|
1fffb3c2e7 | ||
|
|
f0f1f0b365 | ||
|
|
4590a74896 | ||
| be861b823c | |||
|
|
95de8df372 | ||
| 190d1db2fa | |||
| 5994ded8be | |||
|
|
b8a44b45b8 | ||
|
|
7b401add15 | ||
|
|
2d92dd47f0 | ||
|
|
eb8141ee8c | ||
|
|
2a50f5580d | ||
|
|
65bfb1d129 | ||
|
|
234f3d627d | ||
|
|
be1dccfcb9 | ||
|
|
454e5e807a | ||
|
|
29d20d5c5a | ||
|
|
e29546c727 | ||
|
|
5a89be6238 | ||
|
|
cfb5faa09b | ||
| 37471efb74 | |||
|
|
7a4f40a765 | ||
|
|
d11d55db66 | ||
|
|
76b6aa7c15 | ||
|
|
70d4fecad6 | ||
|
|
c3fa91752c | ||
|
|
7c2beef193 | ||
|
|
630d0d80d2 | ||
|
|
695743104c | ||
|
|
d014244b28 | ||
|
|
5cd56259f7 | ||
|
|
fa9f78b46e | ||
|
|
4a5f67f0fd | ||
|
|
3bed9cc27f | ||
|
|
0dcae25b93 | ||
|
|
ca734dcd39 | ||
|
|
9448072112 | ||
|
|
b1e98ca9d7 | ||
|
|
f036d409e6 | ||
|
|
5493c63f18 | ||
|
|
6c3139bf0f | ||
|
|
28ebb69612 | ||
|
|
008cb40cb8 | ||
|
|
195d3a219a | ||
|
|
c6452c4f2b | ||
|
|
3dbacf0d65 | ||
|
|
56a82baf40 | ||
|
|
20d210245b | ||
|
|
21cbcf677a | ||
|
|
8fb355d03b | ||
|
|
218477404f | ||
|
|
766c0bc1b4 | ||
|
|
4a8d0666c2 |
@@ -7,3 +7,6 @@ insert_final_newline = true
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
[*.nix]
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
SENTRY_ORG=longhorn-developers
|
||||||
|
SENTRY_PROJECT=ut-registration-plus
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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**
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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**
|
||||||
|
|||||||
12
.github/ISSUE_TEMPLATE/updating-build-dependencies.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/updating-build-dependencies.md
vendored
Normal 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
21
.github/dependabot.yml
vendored
Normal 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'
|
||||||
8
.github/workflows/best-practices.yml
vendored
8
.github/workflows/best-practices.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/check-types.yml
vendored
4
.github/workflows/check-types.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/chromatic.yml
vendored
4
.github/workflows/chromatic.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -211,3 +211,5 @@ sketch
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
storybook-static/
|
storybook-static/
|
||||||
package/
|
package/
|
||||||
|
|
||||||
|
.direnv/
|
||||||
|
|||||||
663
CHANGELOG.md
663
CHANGELOG.md
@@ -1,335 +1,390 @@
|
|||||||
|
## [2.2.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.1...v2.2.2) (2025-10-13)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add nix flake ([#593](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/593)) ([7b401ad](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7b401add1565ff401bad99745ff9e53b9a7f899f))
|
||||||
|
* automatically select new or duplicated schedules ([#583](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/583)) ([#589](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/589)) ([2a50f55](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2a50f5580d3dbeb0d66546c23cf29bbb37d80da2))
|
||||||
|
* **env:** add SENTRY env vars ([8f7e1bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8f7e1bc0af6336549068e02b80df21d4e8f4ef9c))
|
||||||
|
* export schedule button add to calendar ([#594](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/594)) ([5994ded](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5994ded8be876cb55174d27d3fdb0832b21a0ff9))
|
||||||
|
* search result shading ([#617](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/617)) ([be861b8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be861b823cb2cb7f6f4a1f266351eec3fc1c2f99))
|
||||||
|
* show warning for courses of different semesters ([#570](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/570)) ([2e7dac1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2e7dac1e3eba757231ac07ac966231c08c703a16))
|
||||||
|
* support summer grades, fix summer course parser ([#596](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/596)) ([2d92dd4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d92dd47f00a44b7d48e92a8ffba94480e4e73f9))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix or ignore various eslint warning ([#609](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/609)) ([95de8df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/95de8df37243b6d59625df515a60442f11b7a9d3))
|
||||||
|
* limit height of schedule list dropdown in the extension popup ([#543](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/543)) ([eb8141e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/eb8141ee8c3d32bce901457178d50781b78f86dd))
|
||||||
|
* whitespace wrapping in semester warning ([#629](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/629)) ([46fe591](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/46fe591fa72ef017eea7cfb8aa37d12d8f223926))
|
||||||
|
## [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))
|
||||||
|
* **release:** v2.2.1 ([234f3d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/234f3d627d603adf8555b4d0e93106d198918169))
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -26,8 +26,9 @@
|
|||||||
## Toolchain
|
## Toolchain
|
||||||
|
|
||||||
- React v20.9.0 (LTS)
|
- React v20.9.0 (LTS)
|
||||||
- TypeScript
|
- TypeScript v5.x
|
||||||
- Vite 5
|
- Vite v5.x
|
||||||
|
- pnpm v10.x
|
||||||
- UnoCSS
|
- UnoCSS
|
||||||
- ESLint
|
- ESLint
|
||||||
- Prettier
|
- Prettier
|
||||||
@@ -184,8 +185,9 @@ We maintain a strict code of conduct. By contributing, you agree to adhere to th
|
|||||||
Special thanks to the developers and contributors behind these amazing tools and libraries:
|
Special thanks to the developers and contributors behind these amazing tools and libraries:
|
||||||
|
|
||||||
- React v20.9.0 (LTS)
|
- React v20.9.0 (LTS)
|
||||||
- TypeScript
|
- TypeScript v5.x
|
||||||
- Vite 5
|
- Vite v5.x
|
||||||
|
- pnpm v10.x
|
||||||
- UnoCSS
|
- UnoCSS
|
||||||
- ESLint
|
- ESLint
|
||||||
- Prettier
|
- Prettier
|
||||||
|
|||||||
0
docs/WebSocket-Implementation-Tutorial.md
Normal file
0
docs/WebSocket-Implementation-Tutorial.md
Normal file
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1759831965,
|
||||||
|
"narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c9b6fb798541223bbb396d287d16f43520250518",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
43
flake.nix
Normal file
43
flake.nix
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = (import nixpkgs { inherit system; });
|
||||||
|
|
||||||
|
commonPackages = with pkgs; [
|
||||||
|
nodejs_20 # v20.19.5
|
||||||
|
pnpm_10 # v10.18.0
|
||||||
|
];
|
||||||
|
|
||||||
|
additionalPackages = with pkgs; [
|
||||||
|
bun
|
||||||
|
nodePackages.conventional-changelog-cli
|
||||||
|
sentry-cli
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
formatter = pkgs.nixfmt-rfc-style;
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "utrp-dev";
|
||||||
|
buildInputs = commonPackages;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.full = pkgs.mkShell {
|
||||||
|
name = "utrp-dev-full";
|
||||||
|
buildInputs = commonPackages ++ additionalPackages;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
117
package.json
117
package.json
@@ -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.2",
|
||||||
"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.20",
|
||||||
"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.19.4",
|
||||||
|
"pnpm": "10.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3182
pnpm-lock.yaml
generated
3182
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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
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
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 |
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,14 @@ export default async function createSchedule(scheduleName: string) {
|
|||||||
schedules.push(newSchedule);
|
schedules.push(newSchedule);
|
||||||
|
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
|
||||||
|
// Automatically switch to the new schedule
|
||||||
|
await UserScheduleStore.set('activeIndex', schedules.length - 1);
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -31,5 +31,9 @@ export default async function duplicateSchedule(scheduleId: string): Promise<str
|
|||||||
} satisfies typeof schedule);
|
} satisfies typeof schedule);
|
||||||
|
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
|
||||||
|
// Automatically switch to the duplicated schedule
|
||||||
|
await UserScheduleStore.set('activeIndex', scheduleIndex + 1);
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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 ShadedResults from '@views/components/injected/SearchResultShader';
|
||||||
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 +27,11 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (support === SiteSupport.COURSE_CATALOG_KWS) {
|
||||||
|
renderComponent(ShadedResults);
|
||||||
|
}
|
||||||
|
|||||||
19
src/pages/map/Map.tsx
Normal file
19
src/pages/map/Map.tsx
Normal 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
16
src/pages/map/index.html
Normal 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
6
src/pages/map/index.tsx
Normal 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 />);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
import ReportIssueMain from '@views/components/ReportIssueMain';
|
import ReportIssueMain from '@views/components/ReportIssueMain';
|
||||||
import SentryProvider from '@views/contexts/SentryContext';
|
import SentryProvider from '@views/contexts/SentryContext';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -5,6 +6,8 @@ import { createRoot } from 'react-dom/client';
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<SentryProvider fullInit>
|
<SentryProvider fullInit>
|
||||||
|
<ExtensionRoot>
|
||||||
<ReportIssueMain />
|
<ReportIssueMain />
|
||||||
|
</ExtensionRoot>
|
||||||
</SentryProvider>
|
</SentryProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ 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;
|
||||||
|
|
||||||
|
/** whether the user's email address should be remembered by the extension */
|
||||||
|
rememberMyEmail: boolean;
|
||||||
|
|
||||||
|
/** the user's email address, if set and chosen to be remembered */
|
||||||
|
emailAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionsStore = createSyncStore<IOptionsStore>({
|
export const OptionsStore = createSyncStore<IOptionsStore>({
|
||||||
@@ -26,6 +38,10 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
|
|||||||
enableScrollToLoad: true,
|
enableScrollToLoad: true,
|
||||||
enableDataRefreshing: false,
|
enableDataRefreshing: false,
|
||||||
alwaysOpenCalendarInNewTab: false,
|
alwaysOpenCalendarInNewTab: false,
|
||||||
|
showCalendarSidebar: true,
|
||||||
|
showUTDiningPromo: true,
|
||||||
|
rememberMyEmail: false,
|
||||||
|
emailAddress: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +56,10 @@ 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'),
|
||||||
|
rememberMyEmail: await OptionsStore.get('rememberMyEmail'),
|
||||||
|
emailAddress: await OptionsStore.get('emailAddress'),
|
||||||
}) satisfies IOptionsStore;
|
}) satisfies IOptionsStore;
|
||||||
|
|
||||||
// Clothing retailer right
|
// Clothing retailer right
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export type Semester = {
|
|||||||
export class Course {
|
export class Course {
|
||||||
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
|
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
|
||||||
uniqueId!: number;
|
uniqueId!: number;
|
||||||
/** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */
|
/**
|
||||||
|
* This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H.
|
||||||
|
* UT prefixes summer courses with f, s, n, or w:
|
||||||
|
* [f]irst term, [s]econd term, [n]ine week term, [w]hole term.
|
||||||
|
* So, the first term of PSY 301 over the summer would be 'f301'
|
||||||
|
*/
|
||||||
number!: string;
|
number!: string;
|
||||||
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
|
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
|
||||||
fullName!: string;
|
fullName!: string;
|
||||||
@@ -91,6 +96,46 @@ export class Course {
|
|||||||
}
|
}
|
||||||
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
|
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
|
||||||
this.core = course.core ?? [];
|
this.core = course.core ?? [];
|
||||||
|
if (course.semester.season === 'Summer') {
|
||||||
|
// A bug from and old version put the summer term in the course,
|
||||||
|
// so we need to handle that case
|
||||||
|
const { department, number } = Course.cleanSummerTerm(course.department, course.number);
|
||||||
|
this.department = department;
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due to a bug in an older version, the summer term was included in the course department code,
|
||||||
|
* instead of the course number.
|
||||||
|
* UT prefixes summer courses with f, s, n, or w:
|
||||||
|
* [f]irst term, [s]econd term, [n]ine week term, [w]hole term
|
||||||
|
*
|
||||||
|
* @param department - The course department code, like 'C S'
|
||||||
|
* @param number - The course number, like '314H'
|
||||||
|
* @returns The properly formatted department and course number
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* cleanSummerTerm('C S', '314H') // { department: 'C S', number: '314H' }
|
||||||
|
* cleanSummerTerm('P R', 'f378') // { department: 'P R', number: 'f378' }
|
||||||
|
* cleanSummerTerm('P R f', '378') // { department: 'P R', number: 'f378' }
|
||||||
|
* cleanSummerTerm('P S', 'n303') // { department: 'P S', number: 'n303' }
|
||||||
|
* cleanSummerTerm('P S n', '303') // { department: 'P S', number: 'n303' }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static cleanSummerTerm(department: string, number: string): { department: string; number: string } {
|
||||||
|
// UT prefixes summer courses with f, s, n, or w:
|
||||||
|
// [f]irst term, [s]econd term, [n]ine week term, [w]hole term
|
||||||
|
const summerTerm = department.match(/[fsnw]$/);
|
||||||
|
|
||||||
|
if (!summerTerm) {
|
||||||
|
return { department, number };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
department: department.slice(0, -1).trim(),
|
||||||
|
number: summerTerm[0] + number,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,6 +156,18 @@ export class Course {
|
|||||||
|
|
||||||
return conflicts;
|
return conflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The course number without the summer term
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const c = new Course({ number: 'f301', ... });
|
||||||
|
* c.getNumberWithoutTerm() // '301'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getNumberWithoutTerm(): string {
|
||||||
|
return this.number.replace(/^\D/, ''); // Remove nondigit at start, if it exists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
1486
src/shared/types/MainCampusBuildings.ts
Normal file
1486
src/shared/types/MainCampusBuildings.ts
Normal file
File diff suppressed because it is too large
Load Diff
57
src/shared/types/tests/Course.test.ts
Normal file
57
src/shared/types/tests/Course.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Course } from '../Course';
|
||||||
|
|
||||||
|
describe('Course::cleanSummerTerm', () => {
|
||||||
|
it("shouldn't affect already cleaned summer terms", () => {
|
||||||
|
const inputs = [
|
||||||
|
['C S', '314H'],
|
||||||
|
['P R', 'f378'],
|
||||||
|
['P S', 'f303'],
|
||||||
|
['WGS', 's301'],
|
||||||
|
['S W', 'n360K'],
|
||||||
|
['GOV', 'w312L'],
|
||||||
|
['J', 's311F'],
|
||||||
|
['J S', '311F'],
|
||||||
|
] as const;
|
||||||
|
const expected = [
|
||||||
|
{ department: 'C S', number: '314H' },
|
||||||
|
{ department: 'P R', number: 'f378' },
|
||||||
|
{ department: 'P S', number: 'f303' },
|
||||||
|
{ department: 'WGS', number: 's301' },
|
||||||
|
{ department: 'S W', number: 'n360K' },
|
||||||
|
{ department: 'GOV', number: 'w312L' },
|
||||||
|
{ department: 'J', number: 's311F' },
|
||||||
|
{ department: 'J S', number: '311F' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1]));
|
||||||
|
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move summer term indicator to course number', () => {
|
||||||
|
const inputs = [
|
||||||
|
['P R f', '378'],
|
||||||
|
['P S f', '303'],
|
||||||
|
['WGS s', '301'],
|
||||||
|
['S W n', '360K'],
|
||||||
|
['GOV w', '312L'],
|
||||||
|
['J s', '311F'],
|
||||||
|
['J S', '311F'],
|
||||||
|
] as const;
|
||||||
|
const expected = [
|
||||||
|
{ department: 'P R', number: 'f378' },
|
||||||
|
{ department: 'P S', number: 'f303' },
|
||||||
|
{ department: 'WGS', number: 's301' },
|
||||||
|
{ department: 'S W', number: 'n360K' },
|
||||||
|
{ department: 'GOV', number: 'w312L' },
|
||||||
|
{ department: 'J', number: 's311F' },
|
||||||
|
{ department: 'J S', number: '311F' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1]));
|
||||||
|
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/shared/util/appUrls.ts
Normal file
19
src/shared/util/appUrls.ts
Normal 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();
|
||||||
@@ -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)}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
14
src/stories/components/DiningAppPromo.stories.tsx
Normal file
14
src/stories/components/DiningAppPromo.stories.tsx
Normal 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 = {};
|
||||||
@@ -73,7 +73,7 @@ const generateCourses = (count: number): Course[] => {
|
|||||||
|
|
||||||
const exampleCourses = generateCourses(numberOfCourses);
|
const exampleCourses = generateCourses(numberOfCourses);
|
||||||
|
|
||||||
type CourseWithId = Course & BaseItem;
|
type CourseWithId = { course: Course } & BaseItem;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/SortableList',
|
title: 'Components/Common/SortableList',
|
||||||
@@ -91,11 +91,10 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
draggables: exampleCourses.map(course => ({
|
draggables: exampleCourses.map(course => ({
|
||||||
id: course.uniqueId,
|
id: course.uniqueId,
|
||||||
...course,
|
course,
|
||||||
getConflicts: course.getConflicts,
|
|
||||||
})),
|
})),
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
renderItem: course => <PopupCourseBlock key={course.id} course={course} colors={course.colors} />,
|
renderItem: ({ id, course }) => <PopupCourseBlock key={id} course={course} colors={course.colors} />,
|
||||||
},
|
},
|
||||||
render: args => (
|
render: args => (
|
||||||
<div className='h-3xl w-3xl transform-none'>
|
<div className='h-3xl w-3xl transform-none'>
|
||||||
|
|||||||
@@ -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'],
|
|
||||||
};
|
|
||||||
38
src/stories/components/WhatsNewPopup.stories.tsx
Normal file
38
src/stories/components/WhatsNewPopup.stories.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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/',
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ 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';
|
||||||
|
|
||||||
|
import DialogProvider from './common/DialogProvider/DialogProvider';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
|
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
|
||||||
@@ -27,6 +29,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 +47,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 +58,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]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,6 +84,7 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
|
<DialogProvider>
|
||||||
<NewSearchLink />
|
<NewSearchLink />
|
||||||
<RecruitmentBanner />
|
<RecruitmentBanner />
|
||||||
<TableHead>Plus</TableHead>
|
<TableHead>Plus</TableHead>
|
||||||
@@ -90,6 +107,7 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
|
|||||||
afterLeave={() => setSelectedCourse(null)}
|
afterLeave={() => setSelectedCourse(null)}
|
||||||
/>
|
/>
|
||||||
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
||||||
|
</DialogProvider>
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -147,15 +155,14 @@ export default function PopupMain(): JSX.Element {
|
|||||||
<SortableList
|
<SortableList
|
||||||
draggables={activeSchedule.courses.map(course => ({
|
draggables={activeSchedule.courses.map(course => ({
|
||||||
id: course.uniqueId,
|
id: course.uniqueId,
|
||||||
...course,
|
course,
|
||||||
getConflicts: course.getConflicts,
|
|
||||||
}))}
|
}))}
|
||||||
onChange={reordered => {
|
onChange={reordered => {
|
||||||
activeSchedule.courses = reordered.map(({ id: _id, ...course }) => course);
|
activeSchedule.courses = reordered.map(({ course }) => course);
|
||||||
replaceSchedule(getActiveSchedule(), activeSchedule);
|
replaceSchedule(getActiveSchedule(), activeSchedule);
|
||||||
}}
|
}}
|
||||||
renderItem={course => (
|
renderItem={({ id, course }) => (
|
||||||
<PopupCourseBlock key={course.id} course={course} colors={course.colors} />
|
<PopupCourseBlock key={id} course={course} colors={course.colors} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
|
|
||||||
import { captureFeedback } from '@sentry/react';
|
import { captureFeedback } from '@sentry/react';
|
||||||
|
import { OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Button } from './common/Button';
|
import { Button } from './common/Button';
|
||||||
@@ -12,19 +14,57 @@ import Text from './common/Text/Text';
|
|||||||
* @returns The rendered component.
|
* @returns The rendered component.
|
||||||
*/
|
*/
|
||||||
export default function ReportIssueMain(): JSX.Element {
|
export default function ReportIssueMain(): JSX.Element {
|
||||||
const [email, setEmail] = useState('');
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: emailAddress } = useQuery({
|
||||||
|
queryKey: ['settings', 'emailAddress'],
|
||||||
|
queryFn: () => OptionsStore.get('emailAddress'),
|
||||||
|
staleTime: Infinity, // Prevent loading state on refocus
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: setEmailAddress } = useMutation({
|
||||||
|
mutationKey: ['settings', 'emailAddress'],
|
||||||
|
mutationFn: async ({ rememberMyEmail, emailAddress }: { rememberMyEmail: boolean; emailAddress: string }) => {
|
||||||
|
queryClient.setQueryData(['settings', 'emailAddress'], emailAddress);
|
||||||
|
if (rememberMyEmail) {
|
||||||
|
OptionsStore.set('emailAddress', emailAddress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: rememberMyEmail } = useQuery({
|
||||||
|
queryKey: ['settings', 'rememberMyEmail'],
|
||||||
|
queryFn: () => OptionsStore.get('rememberMyEmail'),
|
||||||
|
staleTime: Infinity, // Prevent loading state on refocus
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: setRememberMyEmail } = useMutation({
|
||||||
|
mutationKey: ['settings', 'rememberMyEmail'],
|
||||||
|
mutationFn: async ({ rememberMyEmail, emailAddress }: { rememberMyEmail: boolean; emailAddress: string }) => {
|
||||||
|
queryClient.setQueryData(['settings', 'rememberMyEmail'], rememberMyEmail);
|
||||||
|
OptionsStore.set('rememberMyEmail', rememberMyEmail);
|
||||||
|
|
||||||
|
if (rememberMyEmail) {
|
||||||
|
OptionsStore.set('emailAddress', emailAddress);
|
||||||
|
} else {
|
||||||
|
OptionsStore.set('emailAddress', '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [feedback, setFeedback] = useState('');
|
const [feedback, setFeedback] = useState('');
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
const submitFeedback = async () => {
|
const submitFeedback = async () => {
|
||||||
if (!email || !feedback) {
|
if (!emailAddress || !feedback) {
|
||||||
throw new Error('Email and feedback are required');
|
throw new Error('Email and feedback are required');
|
||||||
}
|
}
|
||||||
// Here you would typically send the feedback to a server
|
|
||||||
await captureFeedback(
|
// Send the feedback to Sentry
|
||||||
|
captureFeedback(
|
||||||
{
|
{
|
||||||
message: feedback || 'No feedback provided',
|
message: feedback || 'No feedback provided',
|
||||||
email,
|
email: emailAddress,
|
||||||
tags: {
|
tags: {
|
||||||
version: chrome.runtime.getManifest().version,
|
version: chrome.runtime.getManifest().version,
|
||||||
},
|
},
|
||||||
@@ -34,16 +74,14 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset form fields and close the dialog
|
// Close the dialog
|
||||||
setEmail('');
|
|
||||||
setFeedback('');
|
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSubmitted) {
|
if (isSubmitted) {
|
||||||
return (
|
return (
|
||||||
<div className='w-80 flex flex-col rounded-lg bg-white p-6 shadow-lg'>
|
<div className='w-92 flex flex-col rounded-lg bg-white p-6 shadow-lg'>
|
||||||
<Text variant='h2' className='mb-4'>
|
<Text variant='h2' className='my-4'>
|
||||||
Thank you
|
Thank you
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant='p' className='mb-6'>
|
<Text variant='p' className='mb-6'>
|
||||||
@@ -56,28 +94,13 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSubmitted) {
|
|
||||||
return (
|
return (
|
||||||
<div className='w-80 bg-white p-6'>
|
<div className='w-92 bg-white p-6'>
|
||||||
<h2 className='mb-4 text-2xl text-orange font-bold'>{`Hook'em Horns!`}</h2>
|
<h2 className='my-4 text-2xl text-ut-burntorange font-bold'>Longhorn Feedback</h2>
|
||||||
<p className='mb-6 text-gray-600'>Your feedback is music to our ears. Thanks for helping us improve!</p>
|
|
||||||
<button
|
|
||||||
className='w-full rounded bg-orange-600 px-4 py-2 text-white font-bold transition duration-300 hover:bg-orange-700'
|
|
||||||
onClick={() => window.close()}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-80 bg-white p-6'>
|
|
||||||
<h2 className='mb-4 text-2xl text-ut-burntorange font-bold'>Longhorn Feedback</h2>
|
|
||||||
<p className='mb-4 text-sm text-ut-black'>Help us make UT Registration Plus even better!</p>
|
<p className='mb-4 text-sm text-ut-black'>Help us make UT Registration Plus even better!</p>
|
||||||
|
|
||||||
<form onSubmit={submitFeedback}>
|
<form onSubmit={submitFeedback}>
|
||||||
<div className='mb-4'>
|
<div className='mb-1'>
|
||||||
<label htmlFor='email' className='mb-1 block text-sm text-ut-black font-medium'>
|
<label htmlFor='email' className='mb-1 block text-sm text-ut-black font-medium'>
|
||||||
Your @utexas.edu email
|
Your @utexas.edu email
|
||||||
</label>
|
</label>
|
||||||
@@ -85,8 +108,13 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
<input
|
<input
|
||||||
type='email'
|
type='email'
|
||||||
id='email'
|
id='email'
|
||||||
value={email}
|
value={emailAddress}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e =>
|
||||||
|
setEmailAddress({
|
||||||
|
emailAddress: e.target.value,
|
||||||
|
rememberMyEmail: rememberMyEmail ?? false,
|
||||||
|
})
|
||||||
|
}
|
||||||
className='w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
|
className='w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
|
||||||
placeholder='bevo@utexas.edu'
|
placeholder='bevo@utexas.edu'
|
||||||
required
|
required
|
||||||
@@ -94,6 +122,23 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-4'>
|
||||||
|
<label className='mb-1 flex cursor-pointer content-center gap-1.25 text-sm text-ut-black font-medium'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='cursor-pointer'
|
||||||
|
checked={rememberMyEmail}
|
||||||
|
onChange={e =>
|
||||||
|
setRememberMyEmail({
|
||||||
|
rememberMyEmail: e.target.checked,
|
||||||
|
emailAddress: emailAddress ?? '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>{' '}
|
||||||
|
Remember my email
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<label htmlFor='feedback' className='mb-1 block text-sm text-ut-black font-medium'>
|
<label htmlFor='feedback' className='mb-1 block text-sm text-ut-black font-medium'>
|
||||||
Your feedback
|
Your feedback
|
||||||
|
|||||||
@@ -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'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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ export default function HexColorEditor({ hexCode, setHexCode }: HexColorEditorPr
|
|||||||
const tagColor = pickFontColor(previewColor.slice(1) as `#${string}`);
|
const tagColor = pickFontColor(previewColor.slice(1) as `#${string}`);
|
||||||
|
|
||||||
const [localHexCode, setLocalHexCode] = React.useState(hexCode);
|
const [localHexCode, setLocalHexCode] = React.useState(hexCode);
|
||||||
const debouncedSetHexCode = useDebounce((value: string) => setHexCode(value), 500);
|
const debouncedSetHexCode = useDebounce(setHexCode, 500);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (hexCode !== localHexCode) {
|
|
||||||
setLocalHexCode(hexCode);
|
setLocalHexCode(hexCode);
|
||||||
}
|
|
||||||
}, [hexCode]);
|
}, [hexCode]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' />
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
||||||
import { CalendarDots, Export, FilePng, Sidebar } from '@phosphor-icons/react';
|
import { CalendarDots, Export, FileCode, FilePng, Sidebar } from '@phosphor-icons/react';
|
||||||
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
|
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
|
||||||
import { Button } from '@views/components/common/Button';
|
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';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { saveAsCal, saveCalAsPng } from '../utils';
|
import { handleExportJson, saveAsCal, saveCalAsPng } from '../utils';
|
||||||
|
|
||||||
interface CalendarHeaderProps {
|
interface CalendarHeaderProps {
|
||||||
sidebarOpen?: boolean;
|
sidebarOpen?: boolean;
|
||||||
@@ -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)',
|
||||||
@@ -94,6 +98,18 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
|||||||
Save as .cal
|
Save as .cal
|
||||||
</Button>
|
</Button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<Button
|
||||||
|
className='w-full flex justify-start'
|
||||||
|
onClick={() => handleExportJson(activeSchedule.id)}
|
||||||
|
color='ut-black'
|
||||||
|
size='small'
|
||||||
|
variant='minimal'
|
||||||
|
icon={FileCode}
|
||||||
|
>
|
||||||
|
Save as .json
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
{/* <MenuItem>
|
{/* <MenuItem>
|
||||||
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
|
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
|
||||||
Export Unique IDs
|
Export Unique IDs
|
||||||
|
|||||||
69
src/views/components/calendar/DiningAppPromo.tsx
Normal file
69
src/views/components/calendar/DiningAppPromo.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { AppStoreLogo, ForkKnife, X as CloseIcon } from '@phosphor-icons/react';
|
||||||
|
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 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ interface LinkItem {
|
|||||||
|
|
||||||
const links: LinkItem[] = [
|
const links: LinkItem[] = [
|
||||||
{
|
{
|
||||||
text: "Spring '25 Course Schedule",
|
text: "Spring '26 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20262/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Fall '25 Course Schedule",
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Course Schedule Archives',
|
text: 'Course Schedule Archives',
|
||||||
@@ -26,14 +30,10 @@ const links: LinkItem[] = [
|
|||||||
text: 'My Degree Audit (IDA)',
|
text: 'My Degree Audit (IDA)',
|
||||||
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
|
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// text: "Summer '24 Course Schedule",
|
text: "'25-'26 Academic Calendar",
|
||||||
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20246/',
|
url: 'https://registrar.utexas.edu/calendars/25-26',
|
||||||
// },
|
},
|
||||||
// {
|
|
||||||
// text: "'24-'25 Academic Calendar",
|
|
||||||
// url: 'https://registrar.utexas.edu/calendars/24-25',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
text: 'Registration Info Sheet (RIS)',
|
text: 'Registration Info Sheet (RIS)',
|
||||||
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',
|
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',
|
||||||
|
|||||||
200
src/views/components/calendar/academic-calendars.ts
Normal file
200
src/views/components/calendar/academic-calendars.ts
Normal 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>>;
|
||||||
@@ -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 <-- 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,38 @@
|
|||||||
|
import { tz, TZDate } from '@date-fns/tz';
|
||||||
|
import exportSchedule from '@pages/background/lib/exportSchedule';
|
||||||
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 +43,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,85 +78,244 @@ 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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves current schedule to JSON that can be imported on other devices.
|
||||||
|
* @param id - Provided schedule ID to download
|
||||||
|
*/
|
||||||
|
export const handleExportJson = async (id: string) => {
|
||||||
|
const jsonString = await exportSchedule(id);
|
||||||
|
if (jsonString) {
|
||||||
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
const schedule = schedules.find(s => s.id === id);
|
||||||
|
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
||||||
|
await downloadBlob(jsonString, 'JSON', fileName);
|
||||||
|
} else {
|
||||||
|
console.error('Error exporting schedule: jsonString is undefined');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the calendar as a PNG image.
|
* Saves the calendar as a PNG image.
|
||||||
*
|
*
|
||||||
* @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 +333,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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
color: #303030;
|
color: #303030;
|
||||||
|
|
||||||
|
// fix font-family on injected pages
|
||||||
|
* {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
|
||||||
[data-rfd-drag-handle-context-id=':r1:'] {
|
[data-rfd-drag-handle-context-id=':r1:'] {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
|||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='border border-ut-offwhite/50 rounded bg-white'>
|
<div className='max-h-[200px] flex flex-col border border-ut-offwhite/50 rounded bg-white'>
|
||||||
<Disclosure defaultOpen={defaultOpen}>
|
<Disclosure defaultOpen={defaultOpen}>
|
||||||
{({ 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%]!'>
|
||||||
@@ -50,17 +54,17 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
|||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as='div'
|
as='div'
|
||||||
className='overflow-hidden'
|
className='flex flex-1 flex-col overflow-y-hidden'
|
||||||
enter='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
enter='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
||||||
enterFrom='max-h-0 opacity-0 p-0.5'
|
enterFrom='max-h-0 opacity-0 p-0.5'
|
||||||
enterTo='max-h-[440px] opacity-100 p-0'
|
enterTo='max-h-[200px] opacity-100 p-0'
|
||||||
leave='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
leave='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
||||||
leaveFrom='max-h-[440px] opacity-100 p-0'
|
leaveFrom='max-h-[200px] opacity-100 p-0'
|
||||||
leaveTo='max-h-0 opacity-0 p-0.5'
|
leaveTo='max-h-0 opacity-0 p-0.5'
|
||||||
>
|
>
|
||||||
<div className='px-3.5 pb-2.5 pt-2'>
|
<DisclosurePanel className='mx-1.75 mb-2.5 mt-2 flex flex-1 flex-col overflow-y-auto'>
|
||||||
<DisclosurePanel>{children}</DisclosurePanel>
|
<div className='mx-1.75'>{children}</div>
|
||||||
</div>
|
</DisclosurePanel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
139
src/views/components/common/WhatsNewPopup.tsx
Normal file
139
src/views/components/common/WhatsNewPopup.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import { validateLoginStatus } from '@shared/util/checkLoginStatus';
|
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
import useSchedules from '@views/hooks/useSchedules';
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
@@ -43,6 +42,8 @@ export default function InjectedButton(): JSX.Element | null {
|
|||||||
await addCourseByURL(activeSchedule, a);
|
await addCourseByURL(activeSchedule, a);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// We'll allow the alert for this WIP feature
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
window.alert('Logged into UT Registrar.');
|
window.alert('Logged into UT Registrar.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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: {
|
||||||
@@ -215,7 +215,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
|||||||
options={{
|
options={{
|
||||||
...chartOptions,
|
...chartOptions,
|
||||||
title: {
|
title: {
|
||||||
text: `There is currently no grade distribution data for ${course.department} ${course.number}`,
|
text: `There is currently no grade distribution data for ${course.department} ${course.getNumberWithoutTerm()}`,
|
||||||
},
|
},
|
||||||
tooltip: { enabled: false },
|
tooltip: { enabled: false },
|
||||||
}}
|
}}
|
||||||
@@ -228,7 +228,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
|||||||
<Text variant='small' className='text-ut-black'>
|
<Text variant='small' className='text-ut-black'>
|
||||||
Grade Distribution for{' '}
|
Grade Distribution for{' '}
|
||||||
<Text variant='small' className='font-extrabold!' as='strong'>
|
<Text variant='small' className='font-extrabold!' as='strong'>
|
||||||
{course.department} {course.number}
|
{course.department} {course.getNumberWithoutTerm()}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<select
|
<select
|
||||||
@@ -260,14 +260,15 @@ 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 && (
|
||||||
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3 text-center'>
|
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3 text-center'>
|
||||||
<Text variant='small' className='text-theme-red'>
|
<Text variant='small' className='text-theme-red'>
|
||||||
We couldn't find {semester !== 'Aggregate' && ` ${semester}`} grades for this
|
We couldn't find {semester !== 'Aggregate' && ` ${semester}`} grades for this
|
||||||
instructor, so here are the grades for all {course.department} {course.number} sections.
|
instructor, so here are the grades for all {course.department}{' '}
|
||||||
|
{course.getNumberWithoutTerm()} sections.
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import createSchedule from '@pages/background/lib/createSchedule';
|
||||||
|
import switchSchedule from '@pages/background/lib/switchSchedule';
|
||||||
import {
|
import {
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
CalendarDots,
|
CalendarDots,
|
||||||
@@ -14,8 +16,10 @@ import { background } from '@shared/messages';
|
|||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import type Instructor from '@shared/types/Instructor';
|
import type Instructor from '@shared/types/Instructor';
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import { englishStringifyList } from '@shared/util/string';
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import { Chip, coreMap, flagMap } from '@views/components/common/Chip';
|
import { Chip, coreMap, flagMap } from '@views/components/common/Chip';
|
||||||
|
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
|
||||||
import Divider from '@views/components/common/Divider';
|
import Divider from '@views/components/common/Divider';
|
||||||
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';
|
||||||
@@ -60,7 +64,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
|||||||
|
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
const lastCopyTime = useRef<number>(0);
|
const lastCopyTime = useRef<number>(0);
|
||||||
|
const showDialog = usePrompt();
|
||||||
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
||||||
|
|
||||||
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
@@ -112,10 +116,78 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddToNewSchedule = async (close: () => void) => {
|
||||||
|
const newScheduleId = await createSchedule(`${course.semester.season} ${course.semester.year}`);
|
||||||
|
switchSchedule(newScheduleId);
|
||||||
|
addCourse({ course, scheduleId: newScheduleId });
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddOrRemoveCourse = async () => {
|
const handleAddOrRemoveCourse = async () => {
|
||||||
|
const uniqueSemesterCodes = [
|
||||||
|
...new Set(
|
||||||
|
activeSchedule.courses
|
||||||
|
.map(course => course.semester.code)
|
||||||
|
.filter((code): code is string => code !== undefined)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
uniqueSemesterCodes.sort();
|
||||||
|
const codeToReadableMap: Record<string, string> = {};
|
||||||
|
activeSchedule.courses.forEach(course => {
|
||||||
|
const { code } = course.semester;
|
||||||
|
if (code) {
|
||||||
|
const readable = `${course.semester.season} ${course.semester.year}`;
|
||||||
|
codeToReadableMap[code] = readable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sortedSemesters = uniqueSemesterCodes
|
||||||
|
.map(code => codeToReadableMap[code])
|
||||||
|
.filter((value): value is string => value !== undefined);
|
||||||
|
const activeSemesters = englishStringifyList(sortedSemesters);
|
||||||
|
|
||||||
if (!activeSchedule) return;
|
if (!activeSchedule) return;
|
||||||
if (!courseAdded) {
|
if (!courseAdded) {
|
||||||
|
const currentSemesterCode = course.semester.code;
|
||||||
|
// Show warning if this course is for a different semester than the selected schedule
|
||||||
|
if (
|
||||||
|
activeSchedule.courses.length > 0 &&
|
||||||
|
activeSchedule.courses.every(otherCourse => otherCourse.semester.code !== currentSemesterCode)
|
||||||
|
) {
|
||||||
|
const dialogButtons = (close: () => void) => (
|
||||||
|
<>
|
||||||
|
<Button variant='minimal' color='ut-black' onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='filled'
|
||||||
|
color='ut-burntorange'
|
||||||
|
onClick={() => {
|
||||||
|
handleAddToNewSchedule(close);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start a new schedule
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
showDialog({
|
||||||
|
title: 'This course section is from a different semester!',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
The section you're adding is for{' '}
|
||||||
|
<span className='text-ut-burntorange whitespace-nowrap'>
|
||||||
|
{course.semester.season} {course.semester.year}
|
||||||
|
</span>
|
||||||
|
, but your current schedule contains sections in{' '}
|
||||||
|
<span className='text-ut-burntorange whitespace-nowrap'>{activeSemesters}</span>. Mixing
|
||||||
|
semesters in one schedule may cause confusion.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
buttons: dialogButtons,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
addCourse({ course, scheduleId: activeSchedule.id });
|
addCourse({ course, scheduleId: activeSchedule.id });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
removeCourse({ course, scheduleId: activeSchedule.id });
|
removeCourse({ course, scheduleId: activeSchedule.id });
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/views/components/injected/DaysCheckbox.tsx
Normal file
89
src/views/components/injected/DaysCheckbox.tsx
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
};
|
};
|
||||||
|
|||||||
39
src/views/components/injected/SearchResultShader.tsx
Normal file
39
src/views/components/injected/SearchResultShader.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
// @TODO Get a better name for this class
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The existing search results (kws), only with alternate shading for easier readability
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default function ShadedResults(): null {
|
||||||
|
useEffect(() => {
|
||||||
|
const table = document.getElementById('kw_results_table');
|
||||||
|
if (!table) {
|
||||||
|
console.error('Results table not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
if (!tbody) {
|
||||||
|
console.error('Table tbody not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
#kw_results_table tbody tr:nth-child(even) {
|
||||||
|
background-color: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
#kw_results_table tbody tr:nth-child(even) td {
|
||||||
|
background-color: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
style.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
795
src/views/components/map/CampusMap.tsx
Normal file
795
src/views/components/map/CampusMap.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/views/components/map/DaySelector.tsx
Normal file
38
src/views/components/map/DaySelector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/views/components/map/DevToggles.tsx
Normal file
101
src/views/components/map/DevToggles.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/views/components/map/FullscreenButton.tsx
Normal file
72
src/views/components/map/FullscreenButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
287
src/views/components/map/Map.tsx
Normal file
287
src/views/components/map/Map.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/views/components/map/Path.tsx
Normal file
61
src/views/components/map/Path.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
193
src/views/components/map/PathFinder.ts
Normal file
193
src/views/components/map/PathFinder.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/views/components/map/PathStats.tsx
Normal file
86
src/views/components/map/PathStats.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
src/views/components/map/TimeWarningLabel.tsx
Normal file
36
src/views/components/map/TimeWarningLabel.tsx
Normal 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}'
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
src/views/components/map/ZoomPanControls.tsx
Normal file
84
src/views/components/map/ZoomPanControls.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1052
src/views/components/map/graphNodes.ts
Normal file
1052
src/views/components/map/graphNodes.ts
Normal file
File diff suppressed because it is too large
Load Diff
124
src/views/components/map/types.ts
Normal file
124
src/views/components/map/types.ts
Normal 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;
|
||||||
111
src/views/components/map/utils.ts
Normal file
111
src/views/components/map/utils.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// import addCourse from '@pages/background/lib/addCourse';
|
// import addCourse from '@pages/background/lib/addCourse';
|
||||||
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
||||||
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
|
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
|
||||||
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 { CRX_PAGES } from '@shared/types/CRXPages';
|
||||||
import { downloadBlob } from '@shared/util/downloadBlob';
|
import MIMEType from '@shared/types/MIMEType';
|
||||||
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
||||||
// import { getCourseColors } from '@shared/util/colors';
|
// import { getCourseColors } from '@shared/util/colors';
|
||||||
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
||||||
@@ -29,6 +29,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import IconoirGitFork from '~icons/iconoir/git-fork';
|
import IconoirGitFork from '~icons/iconoir/git-fork';
|
||||||
|
|
||||||
|
import { handleExportJson } from '../calendar/utils';
|
||||||
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
|
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
|
||||||
import FileUpload from '../common/FileUpload';
|
import FileUpload from '../common/FileUpload';
|
||||||
import { useMigrationDialog } from '../common/MigrationDialog';
|
import { useMigrationDialog } from '../common/MigrationDialog';
|
||||||
@@ -36,14 +37,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 +104,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 +135,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 +195,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);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -215,18 +230,6 @@ export default function Settings(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportClick = async (id: string) => {
|
|
||||||
const jsonString = await exportSchedule(id);
|
|
||||||
if (jsonString) {
|
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
|
||||||
const schedule = schedules.find(s => s.id === id);
|
|
||||||
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
|
||||||
await downloadBlob(jsonString, 'JSON', fileName);
|
|
||||||
} else {
|
|
||||||
console.error('Error exporting schedule: jsonString is undefined');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -255,6 +258,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 />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +386,7 @@ export default function Settings(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='ut-burntorange'
|
color='ut-burntorange'
|
||||||
onClick={() => handleExportClick(activeSchedule.id)}
|
onClick={() => handleExportJson(activeSchedule.id)}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
@@ -397,7 +401,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 +510,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 +581,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 +599,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 +649,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>
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user