Compare commits
64 Commits
v2.1.0
...
derek/wall
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a67a32e4f | ||
|
|
549c52a39f | ||
|
|
36007b9d1a | ||
| cee5f0284f | |||
|
|
e8a8b8e1ae | ||
|
|
c21cbd77f0 | ||
| 99a035e29d | |||
|
|
64baa6d290 | ||
|
|
46fe591fa7 | ||
|
|
8f7e1bc0af | ||
|
|
9fc1098ef7 | ||
|
|
ae094416fc | ||
|
|
2e7dac1e3e | ||
|
|
7bea23a655 | ||
|
|
3d28869e92 | ||
|
|
f0f1f0b365 | ||
| be861b823c | |||
|
|
95de8df372 | ||
| 5994ded8be | |||
|
|
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))
|
||||||
|
|||||||
32
CODE_OF_CONDUCT.md
Normal file
32
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# UT Registration Plus Code of Conduct
|
||||||
|
|
||||||
|
Like the technical community as a whole, the UT Registration Plus team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people.
|
||||||
|
|
||||||
|
Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance.
|
||||||
|
|
||||||
|
This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
|
||||||
|
|
||||||
|
This code of conduct applies to all spaces managed by the UT Registration Plus project or Longhorn Developers. This includes IRC, the mailing lists, the issue tracker, DSF events, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
|
||||||
|
|
||||||
|
If you believe someone is violating the code of conduct, we ask that you report it by emailing [contact@longhorns.dev](mailto:contact@longhorns.dev). For more details please see our
|
||||||
|
|
||||||
|
- **Be friendly and patient.**
|
||||||
|
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
|
||||||
|
- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
|
||||||
|
- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the UT Registration Plus community should be respectful when dealing with other members as well as with people outside the UT Registration Plus community.
|
||||||
|
- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
|
||||||
|
- Violent threats or language directed against another person.
|
||||||
|
- Discriminatory jokes and language.
|
||||||
|
- Posting sexually explicit or violent material.
|
||||||
|
- Posting (or threatening to post) other people's personally identifying information ("doxing").
|
||||||
|
- Personal insults, especially those using racist or sexist terms.
|
||||||
|
- Unwelcome sexual attention.
|
||||||
|
- Advocating for, or encouraging, any of the above behavior.
|
||||||
|
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
|
||||||
|
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and UT Registration Plus is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of UT Registration Plus comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
|
||||||
|
|
||||||
|
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have questions, please see . If that doesn't answer your questions, feel free to [contact us](mailto:contact@longhorns.dev).
|
||||||
@@ -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!",
|
||||||
@@ -114,7 +114,6 @@ const splashText: string[] = [
|
|||||||
'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 />);
|
||||||
@@ -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,14 @@ 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 users are allowed to bypass the 10 schedule limit */
|
||||||
|
allowMoreSchedules: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionsStore = createSyncStore<IOptionsStore>({
|
export const OptionsStore = createSyncStore<IOptionsStore>({
|
||||||
@@ -26,6 +34,9 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
|
|||||||
enableScrollToLoad: true,
|
enableScrollToLoad: true,
|
||||||
enableDataRefreshing: false,
|
enableDataRefreshing: false,
|
||||||
alwaysOpenCalendarInNewTab: false,
|
alwaysOpenCalendarInNewTab: false,
|
||||||
|
showCalendarSidebar: true,
|
||||||
|
showUTDiningPromo: true,
|
||||||
|
allowMoreSchedules: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +51,9 @@ 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'),
|
||||||
|
allowMoreSchedules: await OptionsStore.get('allowMoreSchedules'),
|
||||||
}) 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,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',
|
||||||
|
|||||||
@@ -14,26 +14,30 @@ interface LinkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const links: LinkItem[] = [
|
const links: LinkItem[] = [
|
||||||
|
// {
|
||||||
|
// text: "Fall '25 Course Schedule",
|
||||||
|
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// text: 'Course Schedule Archives',
|
||||||
|
// url: 'https://registrar.utexas.edu/schedules/archive',
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
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: 'Course Schedule Archives',
|
text: 'Other Course Schedules',
|
||||||
url: 'https://registrar.utexas.edu/schedules/archive',
|
url: 'https://registrar.utexas.edu/schedules',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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,9 +1,10 @@
|
|||||||
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';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -16,12 +17,12 @@ interface Props {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
color: ThemeColor;
|
color: ThemeColor;
|
||||||
|
accept?: MIMETypeValue | MIMETypeValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable input button component that follows the Button.tsx consistency
|
* A reusable input button component that follows Button.tsx consistency.
|
||||||
*
|
* Now supports drag-and-drop file uploads (issue #446).
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export default function FileUpload({
|
export default function FileUpload({
|
||||||
className,
|
className,
|
||||||
@@ -34,25 +35,81 @@ 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;
|
||||||
const isIconOnly = !children && !!icon;
|
const isIconOnly = !children && !!icon;
|
||||||
const colorHex = getThemeColorHexByName(color);
|
const colorHex = getThemeColorHexByName(color);
|
||||||
const colorRgb = getThemeColorRgbByName(color)?.join(' ');
|
const colorRgb = getThemeColorRgbByName(color)?.join(' ');
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
// Convert accept array to comma-separated list
|
||||||
|
const acceptValue = Array.isArray(accept) ? accept.join(',') : accept;
|
||||||
|
|
||||||
|
// --- Prevent Chrome from opening the file on drop anywhere else ---
|
||||||
|
useEffect(() => {
|
||||||
|
const preventDefault = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('dragover', preventDefault);
|
||||||
|
window.addEventListener('drop', preventDefault);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('dragover', preventDefault);
|
||||||
|
window.removeEventListener('drop', preventDefault);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
// --- Local drag and drop handlers for this button only -------------
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLLabelElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
const file = event.dataTransfer.files?.[0];
|
||||||
|
if (file && inputRef.current && onChange) {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
inputRef.current.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Trigger change event manually
|
||||||
|
onChange({ target: inputRef.current } as React.ChangeEvent<HTMLInputElement>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLLabelElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!disabled) setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event: React.DragEvent<HTMLLabelElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
style={
|
onDrop={handleDrop}
|
||||||
{
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
style={{
|
||||||
...style,
|
...style,
|
||||||
color: disabled ? 'ut-gray' : colorHex,
|
color: disabled ? 'ut-gray' : colorHex,
|
||||||
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity)`,
|
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity))`,
|
||||||
} satisfies React.CSSProperties
|
}}
|
||||||
}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn transition-colors select-none',
|
||||||
{
|
{
|
||||||
|
'ring-2 ring-offset-2 ring-blue-400': isDragging && !disabled,
|
||||||
'text-white! bg-opacity-100 hover:enabled:shadow-md active:enabled:shadow-sm shadow-black/20':
|
'text-white! bg-opacity-100 hover:enabled:shadow-md active:enabled:shadow-sm shadow-black/20':
|
||||||
variant === 'filled',
|
variant === 'filled',
|
||||||
'bg-opacity-0 border-current hover:enabled:bg-opacity-8 border stroke-width-[1px]':
|
'bg-opacity-0 border-current hover:enabled:bg-opacity-8 border stroke-width-[1px]':
|
||||||
@@ -64,6 +121,7 @@ export default function FileUpload({
|
|||||||
'h-[35px] w-[35px] p-spacing-2': size === 'small' && isIconOnly,
|
'h-[35px] w-[35px] p-spacing-2': size === 'small' && isIconOnly,
|
||||||
'h-6 p-spacing-2': size === 'mini' && !isIconOnly,
|
'h-6 p-spacing-2': size === 'mini' && !isIconOnly,
|
||||||
'h-6 w-6 p-0': size === 'mini' && isIconOnly,
|
'h-6 w-6 p-0': size === 'mini' && isIconOnly,
|
||||||
|
'opacity-60 cursor-not-allowed': disabled,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -78,7 +136,14 @@ export default function FileUpload({
|
|||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<input type='file' className='hidden' disabled={disabled} onChange={disabled ? undefined : onChange} />
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
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='whitespace-nowrap text-ut-burntorange'>
|
||||||
|
{course.semester.season} {course.semester.year}
|
||||||
|
</span>
|
||||||
|
, but your current schedule contains sections in{' '}
|
||||||
|
<span className='whitespace-nowrap text-ut-burntorange'>{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.
|
||||||
*
|
*
|
||||||
@@ -91,6 +92,7 @@ export default function Settings(): JSX.Element {
|
|||||||
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
|
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
|
||||||
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
||||||
const [calendarNewTab, setCalendarNewTab] = useState<boolean>(false);
|
const [calendarNewTab, setCalendarNewTab] = useState<boolean>(false);
|
||||||
|
const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState<boolean>(false);
|
||||||
|
|
||||||
const showMigrationDialog = useMigrationDialog();
|
const showMigrationDialog = useMigrationDialog();
|
||||||
|
|
||||||
@@ -103,6 +105,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();
|
||||||
|
|
||||||
@@ -123,6 +127,7 @@ export default function Settings(): JSX.Element {
|
|||||||
enableScrollToLoad,
|
enableScrollToLoad,
|
||||||
enableDataRefreshing,
|
enableDataRefreshing,
|
||||||
alwaysOpenCalendarInNewTab,
|
alwaysOpenCalendarInNewTab,
|
||||||
|
allowMoreSchedules,
|
||||||
} = await initSettings();
|
} = await initSettings();
|
||||||
setEnableCourseStatusChips(enableCourseStatusChips);
|
setEnableCourseStatusChips(enableCourseStatusChips);
|
||||||
// setShowTimeLocation(enableTimeAndLocationInPopup);
|
// setShowTimeLocation(enableTimeAndLocationInPopup);
|
||||||
@@ -130,8 +135,19 @@ export default function Settings(): JSX.Element {
|
|||||||
setLoadAllCourses(enableScrollToLoad);
|
setLoadAllCourses(enableScrollToLoad);
|
||||||
setEnableDataRefreshing(enableDataRefreshing);
|
setEnableDataRefreshing(enableDataRefreshing);
|
||||||
setCalendarNewTab(alwaysOpenCalendarInNewTab);
|
setCalendarNewTab(alwaysOpenCalendarInNewTab);
|
||||||
|
setIncreaseScheduleLimit(allowMoreSchedules);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -174,6 +190,15 @@ export default function Settings(): JSX.Element {
|
|||||||
// console.log('alwaysOpenCalendarInNewTab', newValue);
|
// console.log('alwaysOpenCalendarInNewTab', newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const l6 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
|
||||||
|
setCalendarNewTab(newValue);
|
||||||
|
// console.log('alwaysOpenCalendarInNewTab', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const l7 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
|
||||||
|
setIncreaseScheduleLimit(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
// Remove listeners when the component is unmounted
|
// Remove listeners when the component is unmounted
|
||||||
return () => {
|
return () => {
|
||||||
OptionsStore.removeListener(l1);
|
OptionsStore.removeListener(l1);
|
||||||
@@ -181,6 +206,10 @@ export default function Settings(): JSX.Element {
|
|||||||
OptionsStore.removeListener(l3);
|
OptionsStore.removeListener(l3);
|
||||||
OptionsStore.removeListener(l4);
|
OptionsStore.removeListener(l4);
|
||||||
OptionsStore.removeListener(l5);
|
OptionsStore.removeListener(l5);
|
||||||
|
OptionsStore.removeListener(l6);
|
||||||
|
OptionsStore.removeListener(l7);
|
||||||
|
|
||||||
|
DevStore.removeListener(ds_l1);
|
||||||
|
|
||||||
window.removeEventListener('keydown', handleKeyPress);
|
window.removeEventListener('keydown', handleKeyPress);
|
||||||
};
|
};
|
||||||
@@ -215,35 +244,20 @@ 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) return;
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async e => {
|
|
||||||
try {
|
|
||||||
const result = e.target?.result as string;
|
|
||||||
const jsonObject = JSON.parse(result);
|
|
||||||
await importSchedule(jsonObject);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Invalid import file!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
await importSchedule(data);
|
||||||
|
alert('Schedule imported successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing schedule:', error);
|
||||||
|
alert('Failed to import schedule. Make sure the file is a valid .json format.');
|
||||||
|
}
|
||||||
|
};
|
||||||
// const handleAddCourseByLink = async () => {
|
// const handleAddCourseByLink = async () => {
|
||||||
// // todo: Use a proper modal instead of a prompt
|
// // todo: Use a proper modal instead of a prompt
|
||||||
// const link: string | null = prompt('Enter course link');
|
// const link: string | null = prompt('Enter course link');
|
||||||
@@ -255,6 +269,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 +397,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 +412,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>
|
||||||
@@ -443,6 +463,25 @@ export default function Settings(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Allow more than 10 schedules
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
Allow bypassing the 10-schedule limit. Intended for advisors or staff who
|
||||||
|
need to create many schedules on behalf of students.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SwitchButton
|
||||||
|
isChecked={increaseScheduleLimit}
|
||||||
|
onChange={() => {
|
||||||
|
setIncreaseScheduleLimit(!increaseScheduleLimit);
|
||||||
|
OptionsStore.set('allowMoreSchedules', !increaseScheduleLimit);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
@@ -501,6 +540,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 +611,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 +629,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 +679,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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { background } from '@shared/messages';
|
||||||
|
import { OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
|
import { CRX_PAGES } from '@shared/types/CRXPages';
|
||||||
import useSchedules from '@views/hooks/useSchedules';
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Button } from '../components/common/Button';
|
import { Button } from '../components/common/Button';
|
||||||
import { usePrompt } from '../components/common/DialogProvider/DialogProvider';
|
import { usePrompt } from '../components/common/DialogProvider/DialogProvider';
|
||||||
@@ -17,29 +20,68 @@ const SCHEDULE_LIMIT = 10;
|
|||||||
export function useEnforceScheduleLimit(): () => boolean {
|
export function useEnforceScheduleLimit(): () => boolean {
|
||||||
const [, schedules] = useSchedules();
|
const [, schedules] = useSchedules();
|
||||||
const showDialog = usePrompt();
|
const showDialog = usePrompt();
|
||||||
|
const [allowMoreSchedules, setAllowMoreSchedules] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const val = await OptionsStore.get('allowMoreSchedules');
|
||||||
|
if (mounted) setAllowMoreSchedules(val ?? false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to read allowMoreSchedules from OptionsStore:', err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const listener = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
|
||||||
|
setAllowMoreSchedules(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
OptionsStore.removeListener(listener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
|
// If user has enabled bypass, allow creating more schedules
|
||||||
|
if (allowMoreSchedules) return true;
|
||||||
|
|
||||||
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. You can increase the limit in the settings if it’s really necessary.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
||||||
buttons: close => (
|
buttons: close => (
|
||||||
<Button variant='filled' color='ut-burntorange' onClick={close}>
|
<>
|
||||||
I Understand
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='ut-black'
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
// open options/settings page so user can enable bypass if they are advising staff
|
||||||
|
const url = chrome.runtime.getURL(CRX_PAGES.OPTIONS);
|
||||||
|
background.openNewTab({ url });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant='filled' color='ut-burntorange' onClick={close}>
|
||||||
|
I understand
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, [schedules, showDialog]);
|
}, [schedules, showDialog, allowMoreSchedules]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
54
src/views/hooks/useWhatsNew.tsx
Normal file
54
src/views/hooks/useWhatsNew.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { UT_DINING_APP_STORE_URL } from '@shared/util/appUrls';
|
||||||
|
import { Button } from '@views/components/common/Button';
|
||||||
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
|
||||||
|
import { useDialog } from '@views/contexts/DialogContext';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// import useChangelog from './useChangelog';
|
||||||
|
|
||||||
|
const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that provides a function to display a what's new dialog.
|
||||||
|
*
|
||||||
|
* @returns A function that, when called, shows a dialog with the changelog.
|
||||||
|
*/
|
||||||
|
export default function useWhatsNewPopUp(): () => void {
|
||||||
|
const showDialog = useDialog();
|
||||||
|
// const showChangeLog = useChangelog();
|
||||||
|
// const { version } = chrome.runtime.getManifest();
|
||||||
|
|
||||||
|
const showPopUp = () => {
|
||||||
|
showDialog(close => ({
|
||||||
|
className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8',
|
||||||
|
title: (
|
||||||
|
<div className='flex items-center justify-between gap-4'>
|
||||||
|
<img src={LDIconURL} alt='LD Icon' className='h-12 w-12 rounded-lg' />
|
||||||
|
<Text variant='h1' className='text-theme-black'>
|
||||||
|
Download our new UT Dining app!
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
description: <WhatsNewPopupContent />,
|
||||||
|
buttons: (
|
||||||
|
<div className='flex flex-row items-end gap-spacing-4'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.open(UT_DINING_APP_STORE_URL, '_blank');
|
||||||
|
}}
|
||||||
|
variant='minimal'
|
||||||
|
color='ut-black'
|
||||||
|
>
|
||||||
|
Download UT Dining on iOS
|
||||||
|
</Button>
|
||||||
|
<Button onClick={close} color='ut-burntorange'>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return showPopUp;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user