Compare commits

...

39 Commits

Author SHA1 Message Date
doprz
e13722820b refactor: use a local changelog 2025-10-15 12:22:59 -05:00
doprz
e0067f8a76 fix: delete 2025-10-15 12:20:14 -05:00
doprz
a66fd151dd chore: add Sentry env vars to workflow 2025-10-14 20:25:04 -05:00
doprz
006d375605 fix: scripts and CI/CD 2025-10-14 20:06:46 -05:00
doprz
4bbddacabb revert: use cachix/install-nix-action 2025-10-14 16:36:57 -05:00
doprz
18530d9d45 fix: disable flakehub 2025-10-14 16:32:53 -05:00
doprz
3d3e8ced6f refactor(nix): use DeterminateSystems nix CI GHAs 2025-10-14 16:27:56 -05:00
doprz
c9df1bf344 feat(ci): add GHA jobs for release 2025-10-13 14:12:29 -05:00
doprz
d8216aefb6 feat(ci): add nix job 2025-10-13 13:48:00 -05:00
doprz
aeac1bab5b chore: remove .releaserc.json 2025-10-13 13:24:04 -05:00
doprz
1a0757c3e6 feat: add changesets cli 2025-10-13 13:22:17 -05:00
dependabot[bot]
e8a8b8e1ae chore(deps): bump the npm_and_yarn group across 1 directory with 4 updates (#639)
Bumps the npm_and_yarn group with 4 updates in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers), [brace-expansion](https://github.com/juliangruber/brace-expansion) and [undici](https://github.com/nodejs/undici).


Updates `vite` from 5.4.14 to 5.4.20
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite)

Updates `@babel/helpers` from 7.26.9 to 7.28.4
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-helpers)

Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

Updates `undici` from 6.21.1 to 6.22.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.1...v6.22.0)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.20
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@babel/helpers"
  dependency-version: 7.28.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.22.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 11:36:53 -05:00
doprz
c21cbd77f0 feat(release): v2.2.2 2025-10-13 11:20:01 -05:00
99a035e29d chore: remove summer 2025 schedule list item (#637) 2025-10-13 02:25:27 -05:00
doprz
64baa6d290 refactor(nix): dev shells (#634) 2025-10-12 22:47:47 -05:00
Samuel Gunter
46fe591fa7 fix: whitespace wrapping in semester warning (#629) 2025-10-07 18:28:41 -05:00
doprz
8f7e1bc0af feat(env): add SENTRY env vars 2025-10-07 16:11:14 -05:00
doprz
9fc1098ef7 chore(env): add .env.example 2025-10-07 16:00:40 -05:00
Warith Rahman
ae094416fc chore(ui): added spring 2026 course schedule (#628) 2025-10-07 15:26:15 -05:00
ishita778
2e7dac1e3e feat: show warning for courses of different semesters (#570)
* chore: removed extra space at calendar footer

* chore: fixed eslint issues

* chore: changed return type to react node

* chore: displaycourses true fixes and checks fixed

* chore: prettier fix

* feat: not working same semester course issue

* feat: modifying components to use the new hook

* feat: small fixes

* fix: remove comments and spaces

* fix: dialog error solved

* fix: add to new schedule

* fix: prettier

* fix: delete unnecessary custom hook and p[rettier

* fix: checks all passing

* fix: added requested changes

* fix: added new conditions

* fix: description fixed

* style: fix Roboto Flex not being used as font in dialog

* fix: made requested changes

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
Co-authored-by: Razboy20 <razboy20@gmail.com>
Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-08-27 13:41:36 -05:00
doprz
7bea23a655 refactor: nix flake (#625)
* chore(nix): update flake

* chore(nix): remove just and update comment

* chore(nix): update node version + volta config

* refactor: nix flake
2025-08-25 11:33:44 -07:00
Samuel Gunter
3d28869e92 chore: spring 2025 grades (#624) 2025-08-18 00:29:59 -05:00
doprz
f0f1f0b365 chore: bump node and pnpm version (#620)
* chore(nix): update flake

* chore(nix): remove just and update comment

* chore(nix): update node version + volta config
2025-08-12 14:00:45 -05:00
be861b823c feat: search result shading (#617)
* feat: site support kws

* feat: function

* feat: stuff before bedtime

* feat: shading function

* feat: shading

* feat: shading the table children

* chore: fix lint issues

* feat: dependency array

* feat: remove

* feat: remove temp console log
2025-08-07 13:28:56 -05:00
Samuel Gunter
95de8df372 fix: fix or ignore various eslint warning (#609) 2025-07-16 07:54:40 -07:00
5994ded8be feat: export schedule button add to calendar (#594)
* feat: export schedule button add to calendar add to util too

* docs: hypen bruh

* chore: lowercase

* style: filecode icon

* chore: unused import

* refactor: use export json deleted old function

* chore: linting

* chore: remove useless import

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-06-17 11:57:48 -07:00
doprz
7b401add15 feat: add nix flake (#593)
Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-06-08 23:16:43 -05:00
Samuel Gunter
2d92dd47f0 feat: support summer grades, fix summer course parser (#596)
* feat: support summer grades, fix summer course parser

* chore: lint

* docs: mention summer terms in Course::number description

* feat: Course::getNumberWithoutTerm, strip summer term indicator when displaying grades

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-06-08 21:10:05 -07:00
Aiyaz Mostofa
eb8141ee8c fix: limit height of schedule list dropdown in the extension popup (#543)
* fix: limit height of schedule list dropdown in the extension popup

* fix: limit the entire dropdown to 200px, not just the schedule list

* fix: use flexbox for dropdown and wedge scrollbar inside margin

* fix: use DisclosurePanel in schedule dropdown, do Uno class sorting

---------

Co-authored-by: Ethan Lanting <ethanlanting@gmail.com>
Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-06-08 20:54:36 -07:00
sjalkote
2a50f5580d feat: automatically select new or duplicated schedules (#583) (#589)
Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-06-08 20:39:46 -07:00
doprz
65bfb1d129 docs: add pnpm and update versions (#597) 2025-06-08 15:57:12 -05:00
doprz
234f3d627d feat(release): v2.2.1 2025-06-03 19:46:36 -05:00
Ethan Lanting
be1dccfcb9 feat: add dining app promo (#598)
* feat: add DiningAppPromo component and integrate it into Calendar

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

* fix: remove outdated links

* chore: run lint

* chore: run prettier

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

* chore: run lint

* chore: run check types

* fix: correct promo visibility logic in Calendar component

* feat: centralize app store URLs in appUrls.ts

* chore: run lint

* feat: integrate UT Dining promo image

* chore: run lint

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

* fix: convert URLs to URL objects for consistency

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

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

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

---------

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

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

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

* fix: gracefully handle async courses

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

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

* chore: fix typo

* fix: don't use sentry in storybook contexts

* fix: silence, brand

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

---------

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

* feat: just ctrl x ctrl v a few lines

* Update insideJokes.tsx
2025-04-13 21:33:52 -05:00
63 changed files with 1851 additions and 388 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@@ -0,0 +1,5 @@
---
'ut-registration-plus': minor
---
Add CI/CD

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -1,18 +1,23 @@
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,node,react,storybookjs
### macOS ###
### macOS
# General
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
.\_\*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
@@ -22,221 +27,278 @@
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### macOS Patch
# iCloud generated files
\*.icloud
### Node
### Node ###
# Logs
logs
*.log
npm-debug.log*
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.*
.env.\*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-te
node_modulesst
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.\*
### Node Patch
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### react ###
.DS_*
**/*.backup.*
**/*.back.*
### react
*.sublime*
.DS\__
\*\*/_.backup._
\*\*/_.back.\*
_.sublime_
psd
thumb
sketch
### VisualStudioCode
### VisualStudioCode ###
.vscode/*
.vscode/_
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
!.vscode/_.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
\*.vsix
### VisualStudioCode Patch
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
package-lock.json
storybook-static/
package/
# Version control
.git
.gitignore
.gitattributes
# Dependencies
.pnpm-store
!pnpm-lock.yaml
# Testing
coverage
.nyc_output
# OS files
.DS_Store
Thumbs.db
# Docker
Dockerfile
.dockerignore
docker-compose*
docker-compose\*
# Documentation
README.md
CHANGELOG.md
CHANGELOG-local.md
DOCKER_DEV_SETUP.md
docs/

View File

@@ -7,3 +7,6 @@ insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 4
indent_style = space
[*.nix]
indent_size = 2

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
SENTRY_ORG=longhorn-developers
SENTRY_PROJECT=ut-registration-plus
SENTRY_AUTH_TOKEN=

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

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

View File

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

View File

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

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

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

View File

@@ -1,25 +1,77 @@
name: Create Release
name: Release
on:
pull_request:
push:
branches:
- production
- preview
jobs:
build:
name: build extension & create release
runs-on: ubuntu-latest
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@master
- name: Get file permission
run: chmod -R 777 .
- main
- name: Install dependencies
run: npm ci
- name: Release with semantic-release
id: semantic-release
run: npx --no-install semantic-release
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
nix-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- run: nix flake check
release:
name: Release
runs-on: ubuntu-latest
needs: nix-check
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Repo
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Dependencies
run: nix develop .#full --command pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
version: nix develop .#full --command pnpm run version
publish: nix develop .#full --command pnpm run release
commit: 'feat(release): version UTRP'
title: 'feat(release): version UTRP'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_ORG: longhorn-developers
SENTRY_PROJECT: ut-registration-plus
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Get UTRP Version and Tag
if: steps.changesets.outputs.hasChangesets == 'false'
id: version
run: |
VERSION=$(nix develop --command node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
- name: Create GitHub Release with Assets
if: steps.changesets.outputs.hasChangesets == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: Release ${{ steps.version.outputs.tag }}
files: |
package/ut-registration-plus-${{ steps.version.outputs.version }}.zip
generate_release_notes: true
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -211,3 +211,5 @@ sketch
package-lock.json
storybook-static/
package/
.direnv/

View File

@@ -1,37 +0,0 @@
{
"branches": [
"production",
{
"name": "preview",
"channel": "alpha",
"prerelease": "alpha"
}
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "SEMANTIC_VERSION=${nextRelease.version} npm run build"
}
],
[
"@semantic-release/github",
{
"assets": "build/**/artifacts/*.*",
"failComment": false
}
]
]
}

View File

@@ -1,3 +1,35 @@
## [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))
- **release:** v2.2.2 ([c21cbd7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c21cbd77f0764c03a711589ff4f957cb8c936eec))
- 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
@@ -7,6 +39,7 @@
- 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))

View File

@@ -26,8 +26,9 @@
## Toolchain
- React v20.9.0 (LTS)
- TypeScript
- Vite 5
- TypeScript v5.x
- Vite v5.x
- pnpm v10.x
- UnoCSS
- ESLint
- 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:
- React v20.9.0 (LTS)
- TypeScript
- Vite 5
- TypeScript v5.x
- Vite v5.x
- pnpm v10.x
- UnoCSS
- ESLint
- Prettier

61
flake.lock generated Normal file
View 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
View 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;
};
}
);
}

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "ut-registration-plus",
"displayName": "UT Registration Plus",
"version": "2.2.0",
"version": "2.2.2",
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
"private": true,
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
@@ -9,8 +9,13 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:dev": "tsc && NODE_ENV='development' vite build --mode development",
"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",
"changeset": "changeset",
"version": "changeset version && pnpm run generate-changelog",
"release": "pnpm run zip:to-publish && changeset tag",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
@@ -60,6 +65,7 @@
"sql.js": "1.11.0"
},
"devDependencies": {
"@changesets/cli": "^2.29.7",
"@chromatic-com/storybook": "^2.0.2",
"@commitlint/cli": "^19.7.1",
"@commitlint/config-conventional": "^19.7.1",
@@ -142,7 +148,7 @@
"unocss": "^0.63.6",
"unocss-preset-primitives": "0.0.2-beta.1",
"unplugin-icons": "^0.19.3",
"vite": "^5.4.14",
"vite": "^5.4.20",
"vite-plugin-inspect": "^0.8.9",
"vitest": "^2.1.9"
},
@@ -160,7 +166,7 @@
}
},
"volta": {
"node": "20.9.0",
"pnpm": "10.6.5"
"node": "20.19.4",
"pnpm": "10.14.0"
}
}

532
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ interface Props {
* @returns A promise that resolves when the changelog is generated.
* @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-local.md', releaseCount = 1 }: Props): Promise<void> {
try {
// Run the conventional-changelog command to generate changelog
const { stdout, stderr } = await execPromise(

View File

@@ -1,53 +0,0 @@
import prompts from 'prompts';
import { simpleGit } from 'simple-git';
import { getSourceRef } from '../utils/git/getSourceRef';
import { error, info } from '../utils/log';
const git = simpleGit();
const status = await git.status();
if (status.files.length) {
console.log(error('Working directory is not clean, please commit or stash changes before releasing.'));
process.exit(1);
}
const { destinationBranch } = await prompts({
type: 'select',
name: 'destinationBranch',
message: 'What kind of release do you want to create?',
choices: ['preview', 'production'].map(releaseType => ({
title: releaseType,
value: releaseType,
})),
});
const sourceRef = await getSourceRef(destinationBranch);
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to create a ${destinationBranch} release from ${sourceRef}?`,
});
if (!confirm) {
console.log(error('Aborting release.'));
process.exit(1);
}
// we fetch the latest changes from the remote
await git.fetch();
// we checkout the source ref, pull the latest changes and then checkout the destination branch
console.info(`Checking out and updating ${sourceRef}...`);
await git.checkout(sourceRef);
await git.pull('origin', sourceRef);
console.info(`Checking out and updating ${destinationBranch}...`);
await git.checkout(destinationBranch);
await git.pull('origin', destinationBranch);
// we trigger the release github action by merging the source ref into the destination branch
console.info(`Merging ${sourceRef} into ${destinationBranch}...`);
await git.merge([sourceRef, '--ff-only']);
await git.push('origin', destinationBranch);
console.info(info(`Release to ${destinationBranch} created! Check github for status`));

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@@ -8,15 +8,27 @@ const splashText: string[] = [
'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.",
"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.',
"The year is 2055 and Welch still isn't finished.",
'Motivation dropping faster than ur GPA',
'No Work Happens On PCL 5th Floor.',
'I may be a sophomore in name, but my credit count screams freshman!',
'Pain is temporary, GPA is forever.',
"You've Yee'd Your Last Haw.",
'lol everything is already waitlisted.',
'Could be worse. Could be A&M.',
// 'TeXAs iS BaCK GuYZ',
'mAke iT yOuR tExAS',
'change yOur slogan',
@@ -24,7 +36,7 @@ const splashText: string[] = [
'Does McCombs teach Parseltongue?',
'No Cruce Enfrente Del Bus.',
'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',
'To hike to Kins or not to hike to Kins...',
"C'mon you Longhorns! You want to study forever?",
@@ -39,8 +51,6 @@ const splashText: string[] = [
'Roll for Initiative!',
'The line at the on-campus Starbucks is longer than your course waitlist.',
'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.",
"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?",
@@ -59,14 +69,6 @@ const splashText: string[] = [
'Planner is now acquired by Plus',
'Longhorn Developers is the best UT Student Org',
'The Eiffel Tower is the UT Tower of Paris',
'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!',
"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.',
'UT Registration Plus - The only thing that can make registration worse is not having it',
@@ -112,7 +114,6 @@ const splashText: string[] = [
'Befriend the raccoons on campus',
`It's ${new Date().toLocaleString('en-US', { month: 'long', day: 'numeric' })} and OU still sucks`,
'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;

View File

@@ -37,6 +37,9 @@ export default async function createSchedule(scheduleName: string) {
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);

View File

@@ -31,5 +31,9 @@ export default async function duplicateSchedule(scheduleId: string): Promise<str
} satisfies typeof schedule);
await UserScheduleStore.set('schedules', schedules);
// Automatically switch to the duplicated schedule
await UserScheduleStore.set('activeIndex', scheduleIndex + 1);
return undefined;
}

View File

@@ -1,6 +1,7 @@
import CourseCatalogMain from '@views/components/CourseCatalogMain';
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 React from 'react';
import { createRoot } from 'react-dom/client';
@@ -30,3 +31,7 @@ if (support === SiteSupport.MY_UT) {
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
renderComponent(DaysCheckbox);
}
if (support === SiteSupport.COURSE_CATALOG_KWS) {
renderComponent(ShadedResults);
}

View File

@@ -21,6 +21,9 @@ export interface IOptionsStore {
/** whether the calendar sidebar should be shown when the calendar is opened */
showCalendarSidebar: boolean;
/** whether the promo should be shown */
showUTDiningPromo: boolean;
}
export const OptionsStore = createSyncStore<IOptionsStore>({
@@ -30,6 +33,7 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
enableDataRefreshing: false,
alwaysOpenCalendarInNewTab: false,
showCalendarSidebar: true,
showUTDiningPromo: true,
});
/**
@@ -45,6 +49,7 @@ export const initSettings = async () =>
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
}) satisfies IOptionsStore;
// Clothing retailer right

View File

@@ -44,7 +44,12 @@ export type Semester = {
export class Course {
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
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;
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
fullName!: string;
@@ -91,6 +96,46 @@ export class Course {
}
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
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;
}
/**
* @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
}
}
/**

View 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);
});
});

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ const generateCourses = (count: number): Course[] => {
const exampleCourses = generateCourses(numberOfCourses);
type CourseWithId = Course & BaseItem;
type CourseWithId = { course: Course } & BaseItem;
const meta = {
title: 'Components/Common/SortableList',
@@ -91,11 +91,10 @@ export const Default: Story = {
args: {
draggables: exampleCourses.map(course => ({
id: course.uniqueId,
...course,
getConflicts: course.getConflicts,
course,
})),
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 => (
<div className='h-3xl w-3xl transform-none'>

View File

@@ -15,6 +15,8 @@ import type { SiteSupportType } from '@views/lib/getSiteSupport';
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
import React, { useEffect, useRef, useState } from 'react';
import DialogProvider from './common/DialogProvider/DialogProvider';
interface Props {
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
}
@@ -82,28 +84,30 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
return (
<ExtensionRoot>
<NewSearchLink />
<RecruitmentBanner />
<TableHead>Plus</TableHead>
{rows.map(
row =>
row.course && (
<TableRow
key={row.course.uniqueId}
row={row}
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
activeSchedule={activeSchedule}
onClick={handleRowButtonClick(row.course)}
/>
)
)}
<CourseCatalogInjectedPopup
course={selectedCourse!} // always defined when showPopup is true
show={showPopup}
onClose={() => setShowPopup(false)}
afterLeave={() => setSelectedCourse(null)}
/>
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
<DialogProvider>
<NewSearchLink />
<RecruitmentBanner />
<TableHead>Plus</TableHead>
{rows.map(
row =>
row.course && (
<TableRow
key={row.course.uniqueId}
row={row}
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
activeSchedule={activeSchedule}
onClick={handleRowButtonClick(row.course)}
/>
)
)}
<CourseCatalogInjectedPopup
course={selectedCourse!} // always defined when showPopup is true
show={showPopup}
onClose={() => setShowPopup(false)}
afterLeave={() => setSelectedCourse(null)}
/>
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
</DialogProvider>
</ExtensionRoot>
);
}

View File

@@ -155,15 +155,14 @@ export default function PopupMain(): JSX.Element {
<SortableList
draggables={activeSchedule.courses.map(course => ({
id: course.uniqueId,
...course,
getConflicts: course.getConflicts,
course,
}))}
onChange={reordered => {
activeSchedule.courses = reordered.map(({ id: _id, ...course }) => course);
activeSchedule.courses = reordered.map(({ course }) => course);
replaceSchedule(getActiveSchedule(), activeSchedule);
}}
renderItem={course => (
<PopupCourseBlock key={course.id} course={course} colors={course.colors} />
renderItem={({ id, course }) => (
<PopupCourseBlock key={id} course={course} colors={course.colors} />
)}
/>
)}

View File

@@ -27,6 +27,7 @@ import { Button } from '../common/Button';
import { LargeLogo } from '../common/LogoIcon';
import Text from '../common/Text/Text';
import CalendarFooter from './CalendarFooter';
import DiningAppPromo from './DiningAppPromo';
/**
* Calendar page component
@@ -41,6 +42,8 @@ export default function Calendar(): ReactNode {
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
const showWhatsNewDialog = useWhatsNewPopUp();
const [showUTDiningPromo, setShowUTDiningPromo] = useState<boolean>(false);
const queryClient = useQueryClient();
const { data: showSidebar, isPending: isSidebarStatePending } = useQuery({
queryKey: ['settings', 'showCalendarSidebar'],
@@ -82,12 +85,19 @@ export default function Calendar(): ReactNode {
if (course) setShowPopup(true);
}, [course]);
useEffect(() => {
// Load the user's preference for the promo
OptionsStore.get('showUTDiningPromo').then(show => {
setShowUTDiningPromo(show);
});
}, []);
if (isSidebarStatePending) return null;
return (
<CalendarContext.Provider value>
<div className='h-full w-full flex flex-col'>
<div className='h-screen flex overflow-auto screenshot:calendar-target'>
<div className='screenshot:calendar-target h-screen flex overflow-auto'>
<div
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',
@@ -122,8 +132,16 @@ export default function Calendar(): ReactNode {
<CalendarSchedules />
<Divider orientation='horizontal' size='100%' />
<ResourceLinks />
<Divider orientation='horizontal' size='100%' />
{/* <TeamLinks /> */}
<Divider orientation='horizontal' size='100%' />
{showUTDiningPromo && (
<DiningAppPromo
onClose={() => {
setShowUTDiningPromo(false);
OptionsStore.set('showUTDiningPromo', false);
}}
/>
)}
<div className='flex flex-col gap-spacing-3'>
<a
href={CRX_PAGES.REPORT}

View File

@@ -27,12 +27,10 @@ export default function HexColorEditor({ hexCode, setHexCode }: HexColorEditorPr
const tagColor = pickFontColor(previewColor.slice(1) as `#${string}`);
const [localHexCode, setLocalHexCode] = React.useState(hexCode);
const debouncedSetHexCode = useDebounce((value: string) => setHexCode(value), 500);
const debouncedSetHexCode = useDebounce(setHexCode, 500);
React.useEffect(() => {
if (hexCode !== localHexCode) {
setLocalHexCode(hexCode);
}
setLocalHexCode(hexCode);
}, [hexCode]);
React.useEffect(() => {

View File

@@ -2,14 +2,18 @@ import type { Course } from '@shared/types/Course';
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
import Text from '@views/components/common/Text/Text';
import { ColorPickerProvider } from '@views/contexts/ColorPickerContext';
import { useSentryScope } from '@views/contexts/SentryContext';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import React, { Fragment } from 'react';
import CalendarCell from './CalendarGridCell';
import { calculateCourseCellColumns } from './utils';
const daysOfWeek = ['MON', 'TUE', 'WED', 'THU', 'FRI'];
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
const IS_STORYBOOK = import.meta.env.STORYBOOK;
interface Props {
courseCells?: CalendarGridCourse[];
saturdayClass?: boolean;
@@ -106,6 +110,12 @@ interface AccountForCourseConflictsProps {
// TODO: Possibly refactor to be more concise
// TODO: Deal with react strict mode (wacky movements)
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
const days = courseCells.reduce(
(acc, cell: CalendarGridCourse) => {
@@ -120,31 +130,15 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
);
// Check for overlaps within each day and adjust gridColumnIndex and totalColumns
Object.values(days).forEach((dayCells: CalendarGridCourse[]) => {
// Sort by start time to ensure proper columnIndex assignment
dayCells.sort((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
dayCells.forEach((cell, _, arr) => {
let columnIndex = 1;
cell.totalColumns = 1;
// Check for overlaps and adjust columnIndex as needed
for (let otherCell of arr) {
if (otherCell !== cell) {
const isOverlapping =
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
if (isOverlapping) {
// Adjust columnIndex to not overlap with the otherCell
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
columnIndex = otherCell.gridColumnStart + 1;
}
cell.totalColumns += 1;
}
}
Object.values(days).forEach((dayCells: CalendarGridCourse[], idx) => {
try {
calculateCourseCellColumns(dayCells);
} catch (error) {
console.error(`Error calculating course cell columns ${idx}`, error);
if (sentryScope) {
sentryScope.captureException(error);
}
cell.gridColumnStart = columnIndex;
cell.gridColumnEnd = columnIndex + 1;
});
}
});
return courseCells

View File

@@ -1,5 +1,5 @@
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 { Button } from '@views/components/common/Button';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
@@ -11,7 +11,7 @@ import useSchedules from '@views/hooks/useSchedules';
import clsx from 'clsx';
import React from 'react';
import { saveAsCal, saveCalAsPng } from '../utils';
import { handleExportJson, saveAsCal, saveCalAsPng } from '../utils';
interface CalendarHeaderProps {
sidebarOpen?: boolean;
@@ -98,6 +98,18 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
Save as .cal
</Button>
</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>
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
Export Unique IDs

View 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>
);
}

View File

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

View File

@@ -14,18 +14,14 @@ interface LinkItem {
}
const links: LinkItem[] = [
{
text: "Spring '26 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20262/',
},
{
text: "Fall '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
},
{
text: "Summer '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
},
{
text: "Spring '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
},
{
text: 'Course Schedule Archives',
url: 'https://registrar.utexas.edu/schedules/archive',
@@ -34,10 +30,10 @@ const links: LinkItem[] = [
text: 'My Degree Audit (IDA)',
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
},
// {
// text: "'24-'25 Academic Calendar",
// url: 'https://registrar.utexas.edu/calendars/24-25',
// },
{
text: "'25-'26 Academic Calendar",
url: 'https://registrar.utexas.edu/calendars/25-26',
},
{
text: 'Registration Info Sheet (RIS)',
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',

View File

@@ -1,6 +1,7 @@
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 {
@@ -8,9 +9,17 @@ import {
multiMeetingMultiInstructorCourse,
multiMeetingMultiInstructorSchedule,
} from 'src/stories/injected/mocked';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { allDatesInRanges, formatToHHMMSS, meetingToIcsString, nextDayInclusive, scheduleToIcsString } 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';
@@ -477,3 +486,196 @@ describe('scheduleToIcsString', () => {
vi.restoreAllMocks();
});
});
describe('calculateCourseCellColumns', () => {
let testIdCounter = 0;
const makeCell = (startIndex: number, endIndex: number): CalendarGridCourse => {
if (endIndex <= startIndex && !(startIndex === -1 && endIndex === -1)) {
throw new Error('Test writer error: startIndex must be strictly less than endIndex');
}
const cell = {
calendarGridPoint: {
dayIndex: 1,
startIndex,
endIndex,
},
componentProps: {} as unknown as CalendarCourseCellProps,
course: {} as unknown as Course,
async: false,
} satisfies CalendarGridCourse;
/* eslint no-underscore-dangle: ["error", { "allow": ["__test_id"] }] */
(cell as unknown as { __test_id: number }).__test_id = testIdCounter++;
return cell;
};
/**
* Creates test cases for calculateCourseCellColumns
* @param cellConfigs - Array of [startIndex, endIndex, totalColumns, gridColumnStart, gridColumnEnd]
* @returns Tuple of [cells, expectedCells]
*/
const makeCellsTest = (
cellConfigs: Array<[number, number, number, number, number]>
): [CalendarGridCourse[], CalendarGridCourse[]] => {
// Create cells with only start/end indices
const cells = cellConfigs.map(([startIndex, endIndex]) => makeCell(startIndex, endIndex));
// Create expected cells with all properties set
const expectedCells = structuredClone<CalendarGridCourse[]>(cells);
cellConfigs.forEach((config, index) => {
const [, , totalColumns, gridColumnStart, gridColumnEnd] = config;
expectedCells[index]!.totalColumns = totalColumns;
expectedCells[index]!.gridColumnStart = gridColumnStart;
expectedCells[index]!.gridColumnEnd = gridColumnEnd;
});
return [cells, expectedCells];
};
beforeEach(() => {
// Ensure independence between tests
testIdCounter = 0;
});
it('should do nothing to an empty array if no courses are present', () => {
const cells: CalendarGridCourse[] = [];
calculateCourseCellColumns(cells);
expect(cells).toEqual([]);
});
it('should set the right values for one course cell', () => {
const [cells, expectedCells] = makeCellsTest([[13, 15, 1, 1, 2]]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle two separated courses', () => {
// These two cells can share a column, because they aren't concurrent
const [cells, expectedCells] = makeCellsTest([
[13, 15, 1, 1, 2],
[16, 18, 1, 1, 2],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle two back-to-back courses', () => {
// These two cells can share a column, because they aren't concurrent
const [cells, expectedCells] = makeCellsTest([
[13, 15, 1, 1, 2],
[15, 17, 1, 1, 2],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle two concurrent courses', () => {
// These two cells must be in separate columns, because they are concurrent
const [cells, expectedCells] = makeCellsTest([
[13, 15, 2, 1, 2],
[14, 16, 2, 2, 3],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle a simple grid', () => {
// Two columns
const [cells, expectedCells] = makeCellsTest([
[13, 15, 2, 1, 2], // start in left-most column
[15, 17, 2, 1, 2], // compact into left column
[13, 17, 2, 2, 3], // take up second column
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle a simple grid, flipped', () => {
// Ensures `totalColumns` is calculated correctly
const [cells, expectedCells] = makeCellsTest([
[13, 17, 2, 1, 2],
[15, 17, 2, 2, 3],
[13, 15, 2, 2, 3],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle a weird grid', () => {
// Three columns
const [cells, expectedCells] = makeCellsTest([
[13, 15, 3, 1, 2],
[14, 18, 3, 2, 3],
[14, 16, 3, 3, 4],
[15, 17, 3, 1, 2], // compacted into left-most columns
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle many clean concurrent courses', () => {
// All cells here are concurrent, 8 columns
const [cells, expectedCells] = makeCellsTest([
[10, 16, 8, 1, 2],
[12, 16, 8, 2, 3],
[13, 16, 8, 3, 4],
[13, 16, 8, 4, 5],
[13, 19, 8, 5, 6],
[13, 19, 8, 6, 7],
[14, 18, 8, 7, 8],
[15, 19, 8, 8, 9],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle many clean concurrent courses with one partially-concurrent', () => {
// Despite adding another course, we don't need to increase
// the number of columns, because we can compact
const [cells, expectedCells] = makeCellsTest([
[10, 16, 8, 1, 2],
[11, 15, 8, 2, 3], // new course, only overlaps with some
[12, 16, 8, 3, 4],
[13, 16, 8, 4, 5],
[13, 16, 8, 5, 6],
[13, 19, 8, 6, 7],
[13, 19, 8, 7, 8],
[14, 18, 8, 8, 9],
[15, 19, 8, 2, 3], // compacts to be under new course
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it("shouldn't crash on courses without times", () => {
const cells = [makeCell(-1, -1), makeCell(-1, -1)];
cells[1]!.async = true; // see if we can ignore async and non-async courses without times
const expectedCells = structuredClone<CalendarGridCourse[]>(cells);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
});

View File

@@ -1,4 +1,5 @@
import { tz, TZDate } from '@date-fns/tz';
import exportSchedule from '@pages/background/lib/exportSchedule';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Course } from '@shared/types/Course';
import type { CourseMeeting } from '@shared/types/CourseMeeting';
@@ -6,6 +7,7 @@ import Instructor from '@shared/types/Instructor';
import type { UserSchedule } from '@shared/types/UserSchedule';
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 { DateArg, Day } from 'date-fns';
import {
@@ -260,6 +262,22 @@ export const saveAsCal = async () => {
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.
*
@@ -315,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;
}
};

View File

@@ -6,7 +6,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
const changelog = new URL('/CHANGELOG.md', import.meta.url).href;
const changelog = new URL('/CHANGELOG-local.md', import.meta.url).href;
/**
* Renders a popup component for displaying the changelog.

View File

@@ -15,6 +15,11 @@
@apply font-sans;
color: #303030;
// fix font-family on injected pages
* {
@apply font-sans;
}
[data-rfd-drag-handle-context-id=':r1:'] {
cursor: move;
}

View File

@@ -19,12 +19,12 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
const [activeSchedule] = useSchedules();
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}>
{({ open }) => (
<>
<DisclosureButton className='w-full flex items-center border-none bg-transparent px-3.5 py-2.5 text-left'>
<div className='flex-1 min-w-0 overflow-hidden'>
<div className='min-w-0 flex-1 overflow-hidden'>
<Text
as='div'
variant='h3'
@@ -54,17 +54,17 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
<Transition
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'
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'
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'
>
<div className='px-3.5 pb-2.5 pt-2'>
<DisclosurePanel>{children}</DisclosurePanel>
</div>
<DisclosurePanel className='mx-1.75 mb-2.5 mt-2 flex flex-1 flex-col overflow-y-auto'>
<div className='mx-1.75'>{children}</div>
</DisclosurePanel>
</Transition>
</>
)}

View File

@@ -1,6 +1,7 @@
import type { IconProps } from '@phosphor-icons/react';
import { CloudX, Copy, Exam, MapPinArea, Palette } 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';
@@ -12,9 +13,9 @@ import React, { useEffect, useState } from 'react';
*
* It should be incremented every time the "What's New" popup is updated.
*/
const WHATSNEW_POPUP_VERSION = 1;
const WHATSNEW_POPUP_VERSION = 2;
const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
// const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
type Feature = {
id: string;
@@ -25,35 +26,28 @@ type Feature = {
const NEW_FEATURES = [
{
id: 'custom-course-colors',
icon: Palette,
title: 'Custom Course Colors',
description: 'Paint your schedule in your favorite color theme',
id: 'dining-halls-info',
icon: ForkKnife,
title: 'Dining Halls Info',
description: 'See daily menus and nutritional deets for J2, JCL, and Kins',
},
{
id: 'quick-copy',
icon: Copy,
title: 'Quick Copy',
description: 'Quickly copy a course unique number to your clipboard',
id: 'coffee-shops',
icon: Coffee,
title: 'Coffee Shops',
description: 'Need a Coffee Fix? Check operating times for your favorite campus cafes.',
},
{
id: 'updated-grades',
icon: Exam,
title: 'Updated Grades',
description: 'Fall 2024 grades are now available in the grade distribution',
id: 'convenience-stores',
icon: Storefront,
title: 'Convenience Stores',
description: 'Find hours for quick snacks and essentials on campus.',
},
{
id: 'ut-map',
icon: MapPinArea,
title: (
<div className='flex flex-row items-center'>
UTRP Map
<span className='mx-2 border border-ut-burntorange rounded px-2 py-0.5 text-xs text-ut-burntorange font-medium'>
BETA
</span>
</div>
),
description: 'Find directions to your classes with our beta map feature in the settings page',
id: 'microwave-map',
icon: MapTrifold,
title: 'Microwave Map',
description: 'Need to heat up your lunch? Find microwaves across campus!',
},
] as const satisfies readonly Feature[];
@@ -66,7 +60,7 @@ const NEW_FEATURES = [
* @returns A JSX of WhatsNewPopupContent component.
*/
export default function WhatsNewPopupContent(): JSX.Element {
const [videoError, setVideoError] = useState(false);
const [videoError, _setVideoError] = useState(false);
return (
<div className='w-full flex flex-row justify-between'>
@@ -97,14 +91,11 @@ export default function WhatsNewPopupContent(): JSX.Element {
</div>
</div>
) : (
<video
className='h-fit w-full flex items-center justify-center border border-ut-offwhite/50 rounded object-cover'
autoPlay
loop
muted
>
<source src={WHATSNEW_VIDEO_URL} type='video/mp4' onError={() => setVideoError(true)} />
</video>
<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>

View File

@@ -1,6 +1,5 @@
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
import { background } from '@shared/messages';
import { validateLoginStatus } from '@shared/util/checkLoginStatus';
import { Button } from '@views/components/common/Button';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import useSchedules from '@views/hooks/useSchedules';
@@ -43,6 +42,8 @@ export default function InjectedButton(): JSX.Element | null {
await addCourseByURL(activeSchedule, a);
}
} else {
// We'll allow the alert for this WIP feature
// eslint-disable-next-line no-alert
window.alert('Logged into UT Registrar.');
}
};

View File

@@ -215,7 +215,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
options={{
...chartOptions,
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 },
}}
@@ -228,7 +228,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
<Text variant='small' className='text-ut-black'>
Grade Distribution for{' '}
<Text variant='small' className='font-extrabold!' as='strong'>
{course.department} {course.number}
{course.department} {course.getNumberWithoutTerm()}
</Text>
</Text>
<select
@@ -267,7 +267,8 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
<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'>
We couldn&apos;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>
</div>
)}

View File

@@ -1,3 +1,5 @@
import createSchedule from '@pages/background/lib/createSchedule';
import switchSchedule from '@pages/background/lib/switchSchedule';
import {
ArrowUpRight,
CalendarDots,
@@ -14,8 +16,10 @@ import { background } from '@shared/messages';
import type { Course } from '@shared/types/Course';
import type Instructor from '@shared/types/Instructor';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { englishStringifyList } from '@shared/util/string';
import { Button } from '@views/components/common/Button';
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 Link from '@views/components/common/Link';
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 lastCopyTime = useRef<number>(0);
const showDialog = usePrompt();
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
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 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 (!courseAdded) {
addCourse({ course, scheduleId: activeSchedule.id });
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&apos;re adding is for{' '}
<span className='text-ut-burntorange whitespace-nowrap'>
{course.semester.season} {course.semester.year}
</span>
, but your current schedule contains sections in{' '}
<span className='text-ut-burntorange whitespace-nowrap'>{activeSemesters}</span>. Mixing
semesters in one schedule may cause confusion.
</>
),
buttons: dialogButtons,
});
} else {
addCourse({ course, scheduleId: activeSchedule.id });
}
} else {
removeCourse({ course, scheduleId: activeSchedule.id });
}

View 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;
}

View File

@@ -1,16 +1,13 @@
// import addCourse from '@pages/background/lib/addCourse';
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
import exportSchedule from '@pages/background/lib/exportSchedule';
import importSchedule from '@pages/background/lib/importSchedule';
import { CalendarDots, Trash } from '@phosphor-icons/react';
import { background } from '@shared/messages';
import { DevStore } from '@shared/storage/DevStore';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { CRX_PAGES } from '@shared/types/CRXPages';
import MIMEType from '@shared/types/MIMEType';
import { downloadBlob } from '@shared/util/downloadBlob';
// import { addCourseByUrl } from '@shared/util/courseUtils';
// import { getCourseColors } from '@shared/util/colors';
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
@@ -32,6 +29,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import IconoirGitFork from '~icons/iconoir/git-fork';
import { handleExportJson } from '../calendar/utils';
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
import FileUpload from '../common/FileUpload';
import { useMigrationDialog } from '../common/MigrationDialog';
@@ -232,18 +230,6 @@ export default function Settings(): JSX.Element {
});
};
const handleExportClick = async (id: string) => {
const jsonString = await exportSchedule(id);
if (jsonString) {
const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(s => s.id === id);
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
await downloadBlob(jsonString, 'JSON', fileName);
} else {
console.error('Error exporting schedule: jsonString is undefined');
}
};
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
@@ -400,7 +386,7 @@ export default function Settings(): JSX.Element {
<Button
variant='outline'
color='ut-burntorange'
onClick={() => handleExportClick(activeSchedule.id)}
onClick={() => handleExportJson(activeSchedule.id)}
>
Export
</Button>

View File

@@ -36,6 +36,7 @@ export interface CalendarGridCourse {
gridColumnStart?: number;
gridColumnEnd?: number;
totalColumns?: number;
concurrentCells?: CalendarGridCourse[];
}
/**

View File

@@ -1,11 +1,13 @@
import { UT_DINING_APP_STORE_URL } from '@shared/util/appUrls';
import { Button } from '@views/components/common/Button';
import Text from '@views/components/common/Text/Text';
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
import { useDialog } from '@views/contexts/DialogContext';
import React from 'react';
import { LogoIcon } from '../components/common/LogoIcon';
import useChangelog from './useChangelog';
// 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.
@@ -14,28 +16,34 @@ import useChangelog from './useChangelog';
*/
export default function useWhatsNewPopUp(): () => void {
const showDialog = useDialog();
const showChangeLog = useChangelog();
const { version } = chrome.runtime.getManifest();
// 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'>
<LogoIcon width='48' height='48' />
<img src={LDIconURL} alt='LD Icon' className='h-12 w-12 rounded-lg' />
<Text variant='h1' className='text-theme-black'>
What&apos;s New in UT Registration Plus
Download our new UT Dining app!
</Text>
</div>
),
description: <WhatsNewPopupContent />,
buttons: (
<div className='flex flex-row items-end gap-spacing-4'>
<Button onClick={showChangeLog} variant='minimal' color='ut-black'>
Read Changelog v{version}
<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'>
Get started
Close
</Button>
</div>
),

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { CourseCatalogScraper } from './CourseCatalogScraper';
describe('CourseCatalogScraper::separateCourseName', () => {
it('should separate a simple course', () => {
// UT Formats strings weird... lots of meaningless spaces
const input = 'C S 314H DATA STRUCTURES: HONORS ';
const expected = ['DATA STRUCTURES: HONORS', 'C S', '314H'];
const result = CourseCatalogScraper.separateCourseName(input);
expect(result).toEqual(expected);
});
it('separate summer courses ', () => {
// UT Formats strings weird... lots of meaningless spaces
const inputs = [
'P R f378 PUBLIC RELATNS TECHNIQUES-IRL (First term) ',
'CRP s396 INDEPENDENT RESEARCH IN CRP (Second term) ',
'B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term) ',
'J w379 JOURNALISM INDEPENDENT STUDY (Whole term) ',
];
const expected = [
['PUBLIC RELATNS TECHNIQUES-IRL (First term)', 'P R', 'f378'],
['INDEPENDENT RESEARCH IN CRP (Second term)', 'CRP', 's396'],
['1-MANAGERIAL MICROECON-I-DAL (Nine week term)', 'B A', 'n284S'],
['JOURNALISM INDEPENDENT STUDY (Whole term)', 'J', 'w379'],
];
const results = inputs.map(input => CourseCatalogScraper.separateCourseName(input));
expect(results).toEqual(expected);
});
});

View File

@@ -75,7 +75,7 @@ export class CourseCatalogScraper {
fullName = fullName.replace(/\s\s+/g, ' ').trim();
const [courseName, department, number] = this.separateCourseName(fullName);
const [courseName, department, number] = CourseCatalogScraper.separateCourseName(fullName);
const [status, isReserved] = this.getStatus(row);
const newCourse = new Course({
@@ -113,16 +113,31 @@ export class CourseCatalogScraper {
*
* @example
* ```
* separateCourseName("CS 314H - Honors Discrete Structures") => ["Honors Discrete Structures", "CS", "314H"]
* separateCourseName("C S 314H DATA STRUCTURES: HONORS") => ["DATA STRUCTURES: HONORS", "C S", "314H"]
* ```
* @param courseFullName - the full name of the course (e.g. "CS 314H - Honors Discrete Structures")
* @returns an array of the course name , department, and number
* @param courseFullName - the full name of the course (e.g. "C S 314H DATA STRUCTURES: HONORS")
* @returns an array of the course name, department, and number
*/
separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
let courseNumberIndex = courseFullName.search(/\d/);
let department = courseFullName.substring(0, courseNumberIndex).trim();
let number = courseFullName.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex)).trim();
let courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
static separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
// C S 314H DATA STRUCTURES: HONORS
// ^ Here for normal courses
// B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term)
// ^ Also works for summer courses ([f]irst term, [s]econd term, [n]ine week term, [w]hole term)
const courseNumberIndex = courseFullName.search(/\w?\d/);
if (courseNumberIndex === -1) {
throw new Error("Course name doesn't have a course number");
}
// Everything before the course number
const department = courseFullName.substring(0, courseNumberIndex).trim();
const number = courseFullName
.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex))
.trim();
// Everything after the course number
const courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
return [courseName, department, number];
}

View File

@@ -109,16 +109,22 @@ function generateQuery(
includeInstructor: boolean
): [string, GradeDistributionParams] {
const query = `
select * from grade_distributions
where Department_Code = :department_code
and Course_Number = :course_number
${includeInstructor ? `and Instructor_Last = :instructor_last collate nocase` : ''}
${semester ? `and Semester = :semester` : ''}
SELECT * FROM grade_distributions
WHERE Department_Code = :department_code
AND Course_Number COLLATE NOCASE IN (
:course_number,
concat('F', :course_number), -- Check summer courses with prefix, too
concat('S', :course_number),
concat('N', :course_number),
concat('W', :course_number)
)
${includeInstructor ? `AND Instructor_Last = :instructor_last COLLATE NOCASE` : ''}
${semester ? `AND Semester = :semester` : ''}
`;
const params: GradeDistributionParams = {
':department_code': course.department,
':course_number': course.number,
':course_number': course.getNumberWithoutTerm(),
};
if (includeInstructor) {

View File

@@ -15,6 +15,7 @@ export const SiteSupport = {
MY_UT: 'MY_UT',
COURSE_CATALOG_SEARCH: 'COURSE_CATALOG_SEARCH',
CLASSLIST: 'CLASSLIST',
COURSE_CATALOG_KWS: 'COURSE_CATALOG_KWS',
} as const;
/**
@@ -40,6 +41,9 @@ export default function getSiteSupport(url: string): SiteSupportType | null {
return SiteSupport.UT_PLANNER;
}
if (url.includes('utdirect.utexas.edu/apps/registrar/course_schedule')) {
if (url.includes('kws_results')) {
return SiteSupport.COURSE_CATALOG_KWS;
}
if (url.includes('results')) {
return SiteSupport.COURSE_CATALOG_LIST;
}

View File

@@ -24,16 +24,14 @@ const pagesDir = resolve(root, 'pages');
const assetsDir = resolve(root, 'assets');
const publicDir = resolve(__dirname, 'public');
// Set default environment variables
process.env.PROD = process.env.NODE_ENV === 'production' ? 'true' : 'false';
const isBeta = !!process.env.BETA;
if (isBeta) {
process.env.VITE_BETA_BUILD = 'true';
}
process.env.VITE_PACKAGE_VERSION = packageJson.version;
// TODO: Debug this. If PROD is false, VITE_SENTRY_ENVIRONMENT is in production mode
if (process.env.PROD) {
// special condition for production sentry instrumentation, as many of our devs like to use `pnpm build` directly. Production instrumentation is added and uploaded during `pnpm zip:to-publish`.
if (process.env.SENTRY_ENV === 'production') {
process.env.VITE_SENTRY_ENVIRONMENT = 'production';
} else if (isBeta) {
process.env.VITE_SENTRY_ENVIRONMENT = 'beta';