Compare commits
39 Commits
v2.2.0
...
feature/ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13722820b | ||
|
|
e0067f8a76 | ||
|
|
a66fd151dd | ||
|
|
006d375605 | ||
|
|
4bbddacabb | ||
|
|
18530d9d45 | ||
|
|
3d3e8ced6f | ||
|
|
c9df1bf344 | ||
|
|
d8216aefb6 | ||
|
|
aeac1bab5b | ||
|
|
1a0757c3e6 | ||
|
|
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 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal 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)
|
||||||
5
.changeset/chatty-needles-shave.md
Normal file
5
.changeset/chatty-needles-shave.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'ut-registration-plus': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add CI/CD
|
||||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal 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": []
|
||||||
|
}
|
||||||
122
.dockerignore
122
.dockerignore
@@ -1,18 +1,23 @@
|
|||||||
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
|
# 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
|
# 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
|
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,node,react,storybookjs
|
||||||
|
|
||||||
### macOS ###
|
### macOS
|
||||||
|
|
||||||
# General
|
# General
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
.LSOverride
|
.LSOverride
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
|
||||||
|
.\_\*
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
# Files that might appear in the root of a volume
|
||||||
|
|
||||||
.DocumentRevisions-V100
|
.DocumentRevisions-V100
|
||||||
.fseventsd
|
.fseventsd
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
@@ -22,221 +27,278 @@
|
|||||||
.com.apple.timemachine.donotpresent
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
# Directories potentially created on remote AFP share
|
||||||
|
|
||||||
.AppleDB
|
.AppleDB
|
||||||
.AppleDesktop
|
.AppleDesktop
|
||||||
Network Trash Folder
|
Network Trash Folder
|
||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
### macOS Patch ###
|
### macOS Patch
|
||||||
# iCloud generated files
|
|
||||||
*.icloud
|
# iCloud generated files
|
||||||
|
|
||||||
|
\*.icloud
|
||||||
|
|
||||||
|
### Node
|
||||||
|
|
||||||
### Node ###
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|
||||||
logs
|
logs
|
||||||
*.log
|
_.log
|
||||||
npm-debug.log*
|
npm-debug.log_
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# 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
|
# Runtime data
|
||||||
|
|
||||||
pids
|
pids
|
||||||
*.pid
|
_.pid
|
||||||
*.seed
|
_.seed
|
||||||
*.pid.lock
|
\*.pid.lock
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
*.lcov
|
\*.lcov
|
||||||
|
|
||||||
# nyc test coverage
|
# nyc test coverage
|
||||||
|
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
.grunt
|
.grunt
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
bower_components
|
bower_components
|
||||||
|
|
||||||
# node-waf configuration
|
# node-waf configuration
|
||||||
|
|
||||||
.lock-wscript
|
.lock-wscript
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
build
|
build
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
web_modules/
|
web_modules/
|
||||||
|
|
||||||
# TypeScript cache
|
# TypeScript cache
|
||||||
*.tsbuildinfo
|
|
||||||
|
\*.tsbuildinfo
|
||||||
|
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
|
|
||||||
.npm
|
.npm
|
||||||
|
|
||||||
# Optional eslint cache
|
# Optional eslint cache
|
||||||
|
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
# Optional stylelint cache
|
# Optional stylelint cache
|
||||||
|
|
||||||
.stylelintcache
|
.stylelintcache
|
||||||
|
|
||||||
# Microbundle cache
|
# Microbundle cache
|
||||||
|
|
||||||
.rpt2_cache/
|
.rpt2_cache/
|
||||||
.rts2_cache_cjs/
|
.rts2_cache_cjs/
|
||||||
.rts2_cache_es/
|
.rts2_cache_es/
|
||||||
.rts2_cache_umd/
|
.rts2_cache_umd/
|
||||||
|
|
||||||
# Optional REPL history
|
# Optional REPL history
|
||||||
|
|
||||||
.node_repl_history
|
.node_repl_history
|
||||||
|
|
||||||
# Output of 'npm pack'
|
# Output of 'npm pack'
|
||||||
*.tgz
|
|
||||||
|
\*.tgz
|
||||||
|
|
||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
|
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
.env.*
|
.env.\*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
.cache
|
.cache
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
|
|
||||||
# Next.js build output
|
# Next.js build output
|
||||||
|
|
||||||
.next
|
.next
|
||||||
out
|
out
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
.nuxt
|
.nuxt
|
||||||
dist
|
dist
|
||||||
|
|
||||||
# Gatsby files
|
# Gatsby files
|
||||||
|
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
# 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
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
# public
|
# public
|
||||||
|
|
||||||
# vuepress build output
|
# vuepress build output
|
||||||
|
|
||||||
.vuepress/dist
|
.vuepress/dist
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
.temp
|
.temp
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
.docusaurus
|
.docusaurus
|
||||||
|
|
||||||
# Serverless directories
|
# Serverless directories
|
||||||
|
|
||||||
.serverless/
|
.serverless/
|
||||||
|
|
||||||
# FuseBox cache
|
# FuseBox cache
|
||||||
|
|
||||||
.fusebox/
|
.fusebox/
|
||||||
|
|
||||||
# DynamoDB Local files
|
# DynamoDB Local files
|
||||||
|
|
||||||
.dynamodb/
|
.dynamodb/
|
||||||
|
|
||||||
# TernJS port file
|
# TernJS port file
|
||||||
|
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
.vscode-te
|
.vscode-te
|
||||||
node_modulesst
|
node_modulesst
|
||||||
|
|
||||||
# yarn v2
|
# yarn v2
|
||||||
|
|
||||||
.yarn/cache
|
.yarn/cache
|
||||||
.yarn/unplugged
|
.yarn/unplugged
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.\*
|
||||||
|
|
||||||
|
### Node Patch
|
||||||
|
|
||||||
### Node Patch ###
|
|
||||||
# Serverless Webpack directories
|
# Serverless Webpack directories
|
||||||
|
|
||||||
.webpack/
|
.webpack/
|
||||||
|
|
||||||
# Optional stylelint cache
|
# Optional stylelint cache
|
||||||
|
|
||||||
# SvelteKit build / generate output
|
# SvelteKit build / generate output
|
||||||
|
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
|
|
||||||
### react ###
|
### react
|
||||||
.DS_*
|
|
||||||
**/*.backup.*
|
|
||||||
**/*.back.*
|
|
||||||
|
|
||||||
*.sublime*
|
.DS\__
|
||||||
|
\*\*/_.backup._
|
||||||
|
\*\*/_.back.\*
|
||||||
|
|
||||||
|
_.sublime_
|
||||||
|
|
||||||
psd
|
psd
|
||||||
thumb
|
thumb
|
||||||
sketch
|
sketch
|
||||||
|
|
||||||
|
### VisualStudioCode
|
||||||
|
|
||||||
### VisualStudioCode ###
|
.vscode/_
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
!.vscode/*.code-snippets
|
!.vscode/_.code-snippets
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
# Local History for Visual Studio Code
|
||||||
|
|
||||||
.history/
|
.history/
|
||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
|
||||||
|
|
||||||
### VisualStudioCode Patch ###
|
\*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch
|
||||||
|
|
||||||
# Ignore all local history of files
|
# Ignore all local history of files
|
||||||
|
|
||||||
.history
|
.history
|
||||||
.ionide
|
.ionide
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
|
# 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)
|
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
storybook-static/
|
storybook-static/
|
||||||
package/
|
package/
|
||||||
|
|
||||||
# Version control
|
# Version control
|
||||||
|
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
!pnpm-lock.yaml
|
!pnpm-lock.yaml
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
|
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
docker-compose*
|
docker-compose\*
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
README.md
|
README.md
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
CHANGELOG-local.md
|
||||||
DOCKER_DEV_SETUP.md
|
DOCKER_DEV_SETUP.md
|
||||||
docs/
|
docs/
|
||||||
|
|||||||
@@ -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'
|
||||||
90
.github/workflows/release.yml
vendored
90
.github/workflows/release.yml
vendored
@@ -1,25 +1,77 @@
|
|||||||
name: Create Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- production
|
- main
|
||||||
- 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 .
|
|
||||||
|
|
||||||
- name: Install dependencies
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
run: npm ci
|
|
||||||
- name: Release with semantic-release
|
jobs:
|
||||||
id: semantic-release
|
nix-check:
|
||||||
run: npx --no-install semantic-release
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -211,3 +211,5 @@ sketch
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
storybook-static/
|
storybook-static/
|
||||||
package/
|
package/
|
||||||
|
|
||||||
|
.direnv/
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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)
|
## [2.2.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.1.1...v2.2.0) (2025-04-06)
|
||||||
|
|
||||||
### Features
|
### 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
||||||
|
|
||||||
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
10
gulpfile.js
10
gulpfile.js
@@ -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) {
|
||||||
await exec(`sentry-cli sourcemaps inject ${DIST_DIR}`);
|
if (process.env.SENTRY_ENV && process.env.SENTRY_ENV !== 'development') {
|
||||||
await exec(`sentry-cli sourcemaps upload ${DIST_DIR}`);
|
await exec(`sentry-cli sourcemaps inject ${DIST_DIR}`);
|
||||||
log('Sentry instrumentation completed.');
|
await exec(`sentry-cli sourcemaps upload ${DIST_DIR}`);
|
||||||
|
log('Sentry instrumentation completed.');
|
||||||
|
} else {
|
||||||
|
logWarn('Skipping uploading/creating Sentry source maps. (development build)');
|
||||||
|
}
|
||||||
|
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ut-registration-plus",
|
"name": "ut-registration-plus",
|
||||||
"displayName": "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.",
|
"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,13 @@
|
|||||||
"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",
|
||||||
|
"changeset": "changeset",
|
||||||
|
"version": "changeset version && pnpm run generate-changelog",
|
||||||
|
"release": "pnpm run zip:to-publish && changeset tag",
|
||||||
"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",
|
||||||
@@ -60,6 +65,7 @@
|
|||||||
"sql.js": "1.11.0"
|
"sql.js": "1.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@changesets/cli": "^2.29.7",
|
||||||
"@chromatic-com/storybook": "^2.0.2",
|
"@chromatic-com/storybook": "^2.0.2",
|
||||||
"@commitlint/cli": "^19.7.1",
|
"@commitlint/cli": "^19.7.1",
|
||||||
"@commitlint/config-conventional": "^19.7.1",
|
"@commitlint/config-conventional": "^19.7.1",
|
||||||
@@ -142,7 +148,7 @@
|
|||||||
"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.14",
|
"vite": "^5.4.20",
|
||||||
"vite-plugin-inspect": "^0.8.9",
|
"vite-plugin-inspect": "^0.8.9",
|
||||||
"vitest": "^2.1.9"
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
@@ -160,7 +166,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.9.0",
|
"node": "20.19.4",
|
||||||
"pnpm": "10.6.5"
|
"pnpm": "10.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
532
pnpm-lock.yaml
generated
532
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -30,7 +30,7 @@ interface Props {
|
|||||||
* @returns A promise that resolves when the changelog is generated.
|
* @returns A promise that resolves when the changelog is generated.
|
||||||
* @throws 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-local.md', releaseCount = 1 }: Props): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Run the conventional-changelog command to generate changelog
|
// Run the conventional-changelog command to generate changelog
|
||||||
const { stdout, stderr } = await execPromise(
|
const { stdout, stderr } = await execPromise(
|
||||||
|
|||||||
@@ -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
BIN
src/assets/LD-icon-new.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 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?",
|
||||||
@@ -39,8 +51,6 @@ const splashText: string[] = [
|
|||||||
'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,14 +69,6 @@ const splashText: string[] = [
|
|||||||
'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',
|
|
||||||
'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. 🧢",
|
"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',
|
||||||
@@ -112,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;
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export default async function createSchedule(scheduleName: string) {
|
|||||||
|
|
||||||
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 there is only one schedule, set the active index to the new schedule
|
||||||
if (schedules.length <= 1) {
|
if (schedules.length <= 1) {
|
||||||
await UserScheduleStore.set('activeIndex', 0);
|
await UserScheduleStore.set('activeIndex', 0);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +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 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';
|
||||||
@@ -30,3 +31,7 @@ if (support === SiteSupport.MY_UT) {
|
|||||||
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
|
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
|
||||||
renderComponent(DaysCheckbox);
|
renderComponent(DaysCheckbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (support === SiteSupport.COURSE_CATALOG_KWS) {
|
||||||
|
renderComponent(ShadedResults);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export interface IOptionsStore {
|
|||||||
|
|
||||||
/** whether the calendar sidebar should be shown when the calendar is opened */
|
/** whether the calendar sidebar should be shown when the calendar is opened */
|
||||||
showCalendarSidebar: boolean;
|
showCalendarSidebar: boolean;
|
||||||
|
|
||||||
|
/** whether the promo should be shown */
|
||||||
|
showUTDiningPromo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionsStore = createSyncStore<IOptionsStore>({
|
export const OptionsStore = createSyncStore<IOptionsStore>({
|
||||||
@@ -30,6 +33,7 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
|
|||||||
enableDataRefreshing: false,
|
enableDataRefreshing: false,
|
||||||
alwaysOpenCalendarInNewTab: false,
|
alwaysOpenCalendarInNewTab: false,
|
||||||
showCalendarSidebar: true,
|
showCalendarSidebar: true,
|
||||||
|
showUTDiningPromo: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +49,7 @@ export const initSettings = async () =>
|
|||||||
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'),
|
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
|
||||||
|
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
|
||||||
}) satisfies IOptionsStore;
|
}) satisfies IOptionsStore;
|
||||||
|
|
||||||
// Clothing retailer right
|
// Clothing retailer right
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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();
|
||||||
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'>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type { SiteSupportType } from '@views/lib/getSiteSupport';
|
|||||||
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
|
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
|
||||||
import React, { useEffect, useRef, 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'>;
|
||||||
}
|
}
|
||||||
@@ -82,28 +84,30 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
<NewSearchLink />
|
<DialogProvider>
|
||||||
<RecruitmentBanner />
|
<NewSearchLink />
|
||||||
<TableHead>Plus</TableHead>
|
<RecruitmentBanner />
|
||||||
{rows.map(
|
<TableHead>Plus</TableHead>
|
||||||
row =>
|
{rows.map(
|
||||||
row.course && (
|
row =>
|
||||||
<TableRow
|
row.course && (
|
||||||
key={row.course.uniqueId}
|
<TableRow
|
||||||
row={row}
|
key={row.course.uniqueId}
|
||||||
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
|
row={row}
|
||||||
activeSchedule={activeSchedule}
|
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
|
||||||
onClick={handleRowButtonClick(row.course)}
|
activeSchedule={activeSchedule}
|
||||||
/>
|
onClick={handleRowButtonClick(row.course)}
|
||||||
)
|
/>
|
||||||
)}
|
)
|
||||||
<CourseCatalogInjectedPopup
|
)}
|
||||||
course={selectedCourse!} // always defined when showPopup is true
|
<CourseCatalogInjectedPopup
|
||||||
show={showPopup}
|
course={selectedCourse!} // always defined when showPopup is true
|
||||||
onClose={() => setShowPopup(false)}
|
show={showPopup}
|
||||||
afterLeave={() => setSelectedCourse(null)}
|
onClose={() => setShowPopup(false)}
|
||||||
/>
|
afterLeave={() => setSelectedCourse(null)}
|
||||||
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
/>
|
||||||
|
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
||||||
|
</DialogProvider>
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,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} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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
|
||||||
@@ -41,6 +42,8 @@ export default function Calendar(): ReactNode {
|
|||||||
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
|
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
|
||||||
const showWhatsNewDialog = useWhatsNewPopUp();
|
const showWhatsNewDialog = useWhatsNewPopUp();
|
||||||
|
|
||||||
|
const [showUTDiningPromo, setShowUTDiningPromo] = useState<boolean>(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: showSidebar, isPending: isSidebarStatePending } = useQuery({
|
const { data: showSidebar, isPending: isSidebarStatePending } = useQuery({
|
||||||
queryKey: ['settings', 'showCalendarSidebar'],
|
queryKey: ['settings', 'showCalendarSidebar'],
|
||||||
@@ -82,12 +85,19 @@ export default function Calendar(): ReactNode {
|
|||||||
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;
|
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 screenshot:calendar-target'>
|
<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',
|
||||||
@@ -122,8 +132,16 @@ export default function Calendar(): ReactNode {
|
|||||||
<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'>
|
<div className='flex flex-col gap-spacing-3'>
|
||||||
<a
|
<a
|
||||||
href={CRX_PAGES.REPORT}
|
href={CRX_PAGES.REPORT}
|
||||||
|
|||||||
@@ -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, { Fragment } 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;
|
||||||
@@ -106,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) => {
|
||||||
@@ -120,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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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';
|
||||||
@@ -11,7 +11,7 @@ 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;
|
||||||
@@ -98,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,18 +14,14 @@ interface LinkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const links: LinkItem[] = [
|
const links: LinkItem[] = [
|
||||||
|
{
|
||||||
|
text: "Spring '26 Course Schedule",
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20262/',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "Fall '25 Course Schedule",
|
text: "Fall '25 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: "Summer '25 Course Schedule",
|
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Spring '25 Course Schedule",
|
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: 'Course Schedule Archives',
|
text: 'Course Schedule Archives',
|
||||||
url: 'https://registrar.utexas.edu/schedules/archive',
|
url: 'https://registrar.utexas.edu/schedules/archive',
|
||||||
@@ -34,10 +30,10 @@ const links: LinkItem[] = [
|
|||||||
text: 'My Degree Audit (IDA)',
|
text: 'My Degree Audit (IDA)',
|
||||||
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
|
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// text: "'24-'25 Academic Calendar",
|
text: "'25-'26 Academic Calendar",
|
||||||
// url: 'https://registrar.utexas.edu/calendars/24-25',
|
url: 'https://registrar.utexas.edu/calendars/25-26',
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
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',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { tz } from '@date-fns/tz';
|
import { tz } from '@date-fns/tz';
|
||||||
import { Course } from '@shared/types/Course';
|
import { Course } from '@shared/types/Course';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
||||||
import type { Serialized } from 'chrome-extension-toolkit';
|
import type { Serialized } from 'chrome-extension-toolkit';
|
||||||
import { format as formatDate, parseISO } from 'date-fns';
|
import { format as formatDate, parseISO } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
@@ -8,9 +9,17 @@ import {
|
|||||||
multiMeetingMultiInstructorCourse,
|
multiMeetingMultiInstructorCourse,
|
||||||
multiMeetingMultiInstructorSchedule,
|
multiMeetingMultiInstructorSchedule,
|
||||||
} from 'src/stories/injected/mocked';
|
} 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
|
// Do all timezone calculations relative to UT's timezone
|
||||||
const TIMEZONE = 'America/Chicago';
|
const TIMEZONE = 'America/Chicago';
|
||||||
@@ -477,3 +486,196 @@ describe('scheduleToIcsString', () => {
|
|||||||
vi.restoreAllMocks();
|
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,4 +1,5 @@
|
|||||||
import { tz, TZDate } from '@date-fns/tz';
|
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 { Course } from '@shared/types/Course';
|
||||||
import type { CourseMeeting } from '@shared/types/CourseMeeting';
|
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 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 { 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 type { DateArg, Day } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
@@ -260,6 +262,22 @@ export const saveAsCal = async () => {
|
|||||||
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.
|
||||||
*
|
*
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|||||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
import remarkGfm from 'remark-gfm';
|
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.
|
* Renders a popup component for displaying the changelog.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ 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 min-w-0 overflow-hidden'>
|
<div className='min-w-0 flex-1 overflow-hidden'>
|
||||||
<Text
|
<Text
|
||||||
as='div'
|
as='div'
|
||||||
variant='h3'
|
variant='h3'
|
||||||
@@ -54,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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IconProps } from '@phosphor-icons/react';
|
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 { ExtensionStore } from '@shared/storage/ExtensionStore';
|
||||||
|
import { UT_DINING_PROMO_IMAGE_URL } from '@shared/util/appUrls';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
|
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
|
||||||
import React, { useEffect, useState } from 'react';
|
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.
|
* 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 = {
|
type Feature = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -25,35 +26,28 @@ type Feature = {
|
|||||||
|
|
||||||
const NEW_FEATURES = [
|
const NEW_FEATURES = [
|
||||||
{
|
{
|
||||||
id: 'custom-course-colors',
|
id: 'dining-halls-info',
|
||||||
icon: Palette,
|
icon: ForkKnife,
|
||||||
title: 'Custom Course Colors',
|
title: 'Dining Halls Info',
|
||||||
description: 'Paint your schedule in your favorite color theme',
|
description: 'See daily menus and nutritional deets for J2, JCL, and Kins',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quick-copy',
|
id: 'coffee-shops',
|
||||||
icon: Copy,
|
icon: Coffee,
|
||||||
title: 'Quick Copy',
|
title: 'Coffee Shops',
|
||||||
description: 'Quickly copy a course unique number to your clipboard',
|
description: 'Need a Coffee Fix? Check operating times for your favorite campus cafes.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'updated-grades',
|
id: 'convenience-stores',
|
||||||
icon: Exam,
|
icon: Storefront,
|
||||||
title: 'Updated Grades',
|
title: 'Convenience Stores',
|
||||||
description: 'Fall 2024 grades are now available in the grade distribution',
|
description: 'Find hours for quick snacks and essentials on campus.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ut-map',
|
id: 'microwave-map',
|
||||||
icon: MapPinArea,
|
icon: MapTrifold,
|
||||||
title: (
|
title: 'Microwave Map',
|
||||||
<div className='flex flex-row items-center'>
|
description: 'Need to heat up your lunch? Find microwaves across campus!',
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
] as const satisfies readonly Feature[];
|
] as const satisfies readonly Feature[];
|
||||||
|
|
||||||
@@ -66,7 +60,7 @@ const NEW_FEATURES = [
|
|||||||
* @returns A JSX of WhatsNewPopupContent component.
|
* @returns A JSX of WhatsNewPopupContent component.
|
||||||
*/
|
*/
|
||||||
export default function WhatsNewPopupContent(): JSX.Element {
|
export default function WhatsNewPopupContent(): JSX.Element {
|
||||||
const [videoError, setVideoError] = useState(false);
|
const [videoError, _setVideoError] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full flex flex-row justify-between'>
|
<div className='w-full flex flex-row justify-between'>
|
||||||
@@ -97,14 +91,11 @@ export default function WhatsNewPopupContent(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<video
|
<img
|
||||||
className='h-fit w-full flex items-center justify-center border border-ut-offwhite/50 rounded object-cover'
|
className='h-full w-full border border-ut-offwhite/50 rounded object-cover'
|
||||||
autoPlay
|
src={UT_DINING_PROMO_IMAGE_URL}
|
||||||
loop
|
alt='UT Dining Promo'
|
||||||
muted
|
/>
|
||||||
>
|
|
||||||
<source src={WHATSNEW_VIDEO_URL} type='video/mp4' onError={() => setVideoError(true)} />
|
|
||||||
</video>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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'>
|
<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) {
|
||||||
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'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 {
|
} else {
|
||||||
removeCourse({ course, scheduleId: activeSchedule.id });
|
removeCourse({ course, scheduleId: activeSchedule.id });
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1,16 +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 { 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 { CRX_PAGES } from '@shared/types/CRXPages';
|
||||||
import MIMEType from '@shared/types/MIMEType';
|
import MIMEType from '@shared/types/MIMEType';
|
||||||
import { downloadBlob } from '@shared/util/downloadBlob';
|
|
||||||
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
||||||
// import { getCourseColors } from '@shared/util/colors';
|
// import { getCourseColors } from '@shared/util/colors';
|
||||||
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
// 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 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';
|
||||||
@@ -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 handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -400,7 +386,7 @@ export default function Settings(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='ut-burntorange'
|
color='ut-burntorange'
|
||||||
onClick={() => handleExportClick(activeSchedule.id)}
|
onClick={() => handleExportJson(activeSchedule.id)}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface CalendarGridCourse {
|
|||||||
gridColumnStart?: number;
|
gridColumnStart?: number;
|
||||||
gridColumnEnd?: number;
|
gridColumnEnd?: number;
|
||||||
totalColumns?: number;
|
totalColumns?: number;
|
||||||
|
concurrentCells?: CalendarGridCourse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { UT_DINING_APP_STORE_URL } from '@shared/util/appUrls';
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
|
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
|
||||||
import { useDialog } from '@views/contexts/DialogContext';
|
import { useDialog } from '@views/contexts/DialogContext';
|
||||||
import React from 'react';
|
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.
|
* 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 {
|
export default function useWhatsNewPopUp(): () => void {
|
||||||
const showDialog = useDialog();
|
const showDialog = useDialog();
|
||||||
const showChangeLog = useChangelog();
|
// const showChangeLog = useChangelog();
|
||||||
const { version } = chrome.runtime.getManifest();
|
// const { version } = chrome.runtime.getManifest();
|
||||||
|
|
||||||
const showPopUp = () => {
|
const showPopUp = () => {
|
||||||
showDialog(close => ({
|
showDialog(close => ({
|
||||||
className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8',
|
className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8',
|
||||||
title: (
|
title: (
|
||||||
<div className='flex items-center justify-between gap-4'>
|
<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'>
|
<Text variant='h1' className='text-theme-black'>
|
||||||
What's New in UT Registration Plus
|
Download our new UT Dining app!
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
description: <WhatsNewPopupContent />,
|
description: <WhatsNewPopupContent />,
|
||||||
buttons: (
|
buttons: (
|
||||||
<div className='flex flex-row items-end gap-spacing-4'>
|
<div className='flex flex-row items-end gap-spacing-4'>
|
||||||
<Button onClick={showChangeLog} variant='minimal' color='ut-black'>
|
<Button
|
||||||
Read Changelog v{version}
|
onClick={() => {
|
||||||
|
window.open(UT_DINING_APP_STORE_URL, '_blank');
|
||||||
|
}}
|
||||||
|
variant='minimal'
|
||||||
|
color='ut-black'
|
||||||
|
>
|
||||||
|
Download UT Dining on iOS
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={close} color='ut-burntorange'>
|
<Button onClick={close} color='ut-burntorange'>
|
||||||
Get started
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
34
src/views/lib/CourseCatalogScraper.test.ts
Normal file
34
src/views/lib/CourseCatalogScraper.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,7 +75,7 @@ export class CourseCatalogScraper {
|
|||||||
|
|
||||||
fullName = fullName.replace(/\s\s+/g, ' ').trim();
|
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 [status, isReserved] = this.getStatus(row);
|
||||||
|
|
||||||
const newCourse = new Course({
|
const newCourse = new Course({
|
||||||
@@ -113,16 +113,31 @@ export class CourseCatalogScraper {
|
|||||||
*
|
*
|
||||||
* @example
|
* @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")
|
* @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
|
* @returns an array of the course name, department, and number
|
||||||
*/
|
*/
|
||||||
separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
static separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
||||||
let courseNumberIndex = courseFullName.search(/\d/);
|
// C S 314H DATA STRUCTURES: HONORS
|
||||||
let department = courseFullName.substring(0, courseNumberIndex).trim();
|
// ^ Here for normal courses
|
||||||
let number = courseFullName.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
// B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term)
|
||||||
let courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
// ^ 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];
|
return [courseName, department, number];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,16 +109,22 @@ function generateQuery(
|
|||||||
includeInstructor: boolean
|
includeInstructor: boolean
|
||||||
): [string, GradeDistributionParams] {
|
): [string, GradeDistributionParams] {
|
||||||
const query = `
|
const query = `
|
||||||
select * from grade_distributions
|
SELECT * FROM grade_distributions
|
||||||
where Department_Code = :department_code
|
WHERE Department_Code = :department_code
|
||||||
and Course_Number = :course_number
|
AND Course_Number COLLATE NOCASE IN (
|
||||||
${includeInstructor ? `and Instructor_Last = :instructor_last collate nocase` : ''}
|
:course_number,
|
||||||
${semester ? `and Semester = :semester` : ''}
|
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 = {
|
const params: GradeDistributionParams = {
|
||||||
':department_code': course.department,
|
':department_code': course.department,
|
||||||
':course_number': course.number,
|
':course_number': course.getNumberWithoutTerm(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeInstructor) {
|
if (includeInstructor) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const SiteSupport = {
|
|||||||
MY_UT: 'MY_UT',
|
MY_UT: 'MY_UT',
|
||||||
COURSE_CATALOG_SEARCH: 'COURSE_CATALOG_SEARCH',
|
COURSE_CATALOG_SEARCH: 'COURSE_CATALOG_SEARCH',
|
||||||
CLASSLIST: 'CLASSLIST',
|
CLASSLIST: 'CLASSLIST',
|
||||||
|
COURSE_CATALOG_KWS: 'COURSE_CATALOG_KWS',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,9 @@ export default function getSiteSupport(url: string): SiteSupportType | null {
|
|||||||
return SiteSupport.UT_PLANNER;
|
return SiteSupport.UT_PLANNER;
|
||||||
}
|
}
|
||||||
if (url.includes('utdirect.utexas.edu/apps/registrar/course_schedule')) {
|
if (url.includes('utdirect.utexas.edu/apps/registrar/course_schedule')) {
|
||||||
|
if (url.includes('kws_results')) {
|
||||||
|
return SiteSupport.COURSE_CATALOG_KWS;
|
||||||
|
}
|
||||||
if (url.includes('results')) {
|
if (url.includes('results')) {
|
||||||
return SiteSupport.COURSE_CATALOG_LIST;
|
return SiteSupport.COURSE_CATALOG_LIST;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,14 @@ const pagesDir = resolve(root, 'pages');
|
|||||||
const assetsDir = resolve(root, 'assets');
|
const assetsDir = resolve(root, 'assets');
|
||||||
const publicDir = resolve(__dirname, 'public');
|
const publicDir = resolve(__dirname, 'public');
|
||||||
|
|
||||||
// Set default environment variables
|
|
||||||
process.env.PROD = process.env.NODE_ENV === 'production' ? 'true' : 'false';
|
|
||||||
|
|
||||||
const isBeta = !!process.env.BETA;
|
const isBeta = !!process.env.BETA;
|
||||||
if (isBeta) {
|
if (isBeta) {
|
||||||
process.env.VITE_BETA_BUILD = 'true';
|
process.env.VITE_BETA_BUILD = 'true';
|
||||||
}
|
}
|
||||||
process.env.VITE_PACKAGE_VERSION = packageJson.version;
|
process.env.VITE_PACKAGE_VERSION = packageJson.version;
|
||||||
// TODO: Debug this. If PROD is false, VITE_SENTRY_ENVIRONMENT is in production mode
|
|
||||||
if (process.env.PROD) {
|
// special condition for production sentry instrumentation, as many of our devs like to use `pnpm build` directly. Production instrumentation is added and uploaded during `pnpm zip:to-publish`.
|
||||||
|
if (process.env.SENTRY_ENV === 'production') {
|
||||||
process.env.VITE_SENTRY_ENVIRONMENT = 'production';
|
process.env.VITE_SENTRY_ENVIRONMENT = 'production';
|
||||||
} else if (isBeta) {
|
} else if (isBeta) {
|
||||||
process.env.VITE_SENTRY_ENVIRONMENT = 'beta';
|
process.env.VITE_SENTRY_ENVIRONMENT = 'beta';
|
||||||
|
|||||||
Reference in New Issue
Block a user