Compare commits

..

22 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
ec63b5e903 chore: update Prettier to v3.6.2
Co-authored-by: Razboy20 <29903962+Razboy20@users.noreply.github.com>
2025-11-17 20:02:45 +00:00
copilot-swe-agent[bot]
3ec42bf207 chore: update ESLint to v9 and migrate to flat config format
Co-authored-by: Razboy20 <29903962+Razboy20@users.noreply.github.com>
2025-11-17 20:01:00 +00:00
copilot-swe-agent[bot]
c4193d52d3 Initial plan 2025-11-17 19:49:35 +00:00
Vincent Situ
6a67a32e4f feat: allow bypassing the 10-schedule limit (#675)
* feat: allow bypassing the 10-schedule limit

* feat: option to bypass 10 schedules- code done, not tested

* feat(ui): option to bypass 10 schedules

* feat(ui): button to lead to settings after reaching max limit

* Delete manifest.json

* Remove cross-env from build:watch script

* Remove cross-env and update caniuse-lite version

Removed cross-env dependency and updated caniuse-lite version.

* chore: lint

---------

Co-authored-by: Derek <derex1987@gmail.com>
2025-11-10 13:39:36 -06:00
Uthman
549c52a39f feat: add drag-and-drop import for schedules (#661)
* testing

* feat: add drag-and-drop support to FileUpload (#446)

* chore: remove testing comment

* chore: fix lint issues

* chore: format FileUpload.tsx with prettier

---------

Co-authored-by: Uthman Ogungbo <uthmanogungbo@Uthmans-MacBook-Pro.local>
Co-authored-by: Uthman Ogungbo <uthmanogungbo@wireless-10-148-166-229.public.utexas.edu>
Co-authored-by: Derek <derex1987@gmail.com>
2025-11-06 23:03:55 -06:00
doprz
36007b9d1a docs: add CODE_OF_CONDUCT.md (#651)
Added a Code of Conduct document to outline community guidelines and expectations.

Co-authored-by: Derek <derex1987@gmail.com>
2025-11-05 16:58:16 -06:00
cee5f0284f feat: condense resourceLinks course schedule (#676)
* chore: remove summer 2025 schedule list item

* feat: compress course schedules into one link

* Revise ResourceLinks with Spring '26 schedule

Updated course schedule links for Spring '26.
2025-11-05 16:38:00 -06:00
dependabot[bot]
e8a8b8e1ae chore(deps): bump the npm_and_yarn group across 1 directory with 4 updates (#639)
Bumps the npm_and_yarn group with 4 updates in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers), [brace-expansion](https://github.com/juliangruber/brace-expansion) and [undici](https://github.com/nodejs/undici).


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

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

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

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

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

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

* chore: fixed eslint issues

* chore: changed return type to react node

* chore: displaycourses true fixes and checks fixed

* chore: prettier fix

* feat: not working same semester course issue

* feat: modifying components to use the new hook

* feat: small fixes

* fix: remove comments and spaces

* fix: dialog error solved

* fix: add to new schedule

* fix: prettier

* fix: delete unnecessary custom hook and p[rettier

* fix: checks all passing

* fix: added requested changes

* fix: added new conditions

* fix: description fixed

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

* fix: made requested changes

---------

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

* chore(nix): remove just and update comment

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

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

* chore(nix): remove just and update comment

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

* feat: function

* feat: stuff before bedtime

* feat: shading function

* feat: shading

* feat: shading the table children

* chore: fix lint issues

* feat: dependency array

* feat: remove

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

* docs: hypen bruh

* chore: lowercase

* style: filecode icon

* chore: unused import

* refactor: use export json deleted old function

* chore: linting

* chore: remove useless import

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-06-17 11:57:48 -07:00
31 changed files with 2243 additions and 1510 deletions

3
.env.example Normal file
View File

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

View File

@@ -1,96 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories and management
node_modules/
jspm_packages/
package.json
package-lock.json
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# next.js build output
.next
# Webpack-built output
/dist
# Extension archives
/build
# VsCode
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json
# macOS
.DS_Store
# Terraform
.terraform
# development dependencies
.dev/vue-devtools
.dev/browser-profiles
# IntelliJ
.idea
# Sylelint IntelliJ Plugin Requirement
.stylelintrc.json
# Local environment settings
.env.local
*.svg
config
.eslintrc.js
!.storybook

View File

@@ -1,232 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es6: true,
node: true,
webextensions: true,
},
ignorePatterns: ['*.html', 'tsconfig.json'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
'airbnb-base',
'airbnb/rules/react',
'airbnb-typescript',
'@unocss',
'prettier',
],
plugins: [
'import',
'import-essentials',
'jsdoc',
'eslint-plugin-tsdoc',
'react-prefer-function-component',
'@typescript-eslint',
'simple-import-sort',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
debugger: true,
browser: true,
context: true,
JSX: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
modules: true,
experimentalObjectRestSpread: true,
},
},
settings: {
react: {
version: 'detect',
},
jsdoc: {
mode: 'typescript',
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
},
},
rules: {
'prefer-const': [
'off',
{
destructuring: 'any',
ignoreReadBeforeAssign: false,
},
],
'no-plusplus': 'off',
'no-inner-declarations': 'off',
'sort-imports': 'off',
'no-case-declarations': 'off',
'no-unreachable': 'warn',
'no-constant-condition': 'error',
'space-before-function-paren': 'off',
'no-undef': 'off',
'no-return-await': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/no-shadow': ['off'],
'@typescript-eslint/no-use-before-define': ['off'],
'class-methods-use-this': 'off',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/lines-between-class-members': 'off',
'no-param-reassign': [
'error',
{
props: false,
},
],
'no-console': 'off',
'consistent-return': 'off',
'react/destructuring-assignment': 'off',
'import/prefer-default-export': 'off',
'no-promise-executor-return': 'off',
'import/no-cycle': 'off',
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'react/jsx-no-useless-fragment': [
'error',
{
allowExpressions: true,
},
],
'keyword-spacing': [
'error',
{
before: true,
after: true,
},
],
'no-continue': 'off',
'space-before-blocks': [
'error',
{
functions: 'always',
keywords: 'always',
classes: 'always',
},
],
'react/jsx-filename-extension': [
1,
{
extensions: ['.tsx'],
},
],
'react/no-deprecated': 'warn',
'react/prop-types': 'off',
'react-prefer-function-component/react-prefer-function-component': [
'warn',
{
allowComponentDidCatch: false,
},
],
'react/function-component-definition': 'off',
'react/button-has-type': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-returns-type': 'off',
'jsdoc/newline-after-description': 'off',
'react/require-default-props': 'off',
'jsdoc/require-jsdoc': [
'error',
{
enableFixer: false,
publicOnly: true,
checkConstructors: false,
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionExpression: true,
},
contexts: [
'MethodDefinition:not([key.name="componentDidMount"]):not([key.name="render"])',
'ArrowFunctionExpression',
'ClassDeclaration',
'ClassExpression',
'ClassProperty:not([key.name="state"]):not([key.name="componentDidMount"])',
'FunctionDeclaration',
'FunctionExpression',
'TSDeclareFunction',
'TSEnumDeclaration',
'TSInterfaceDeclaration',
'TSMethodSignature',
'TSModuleDeclaration',
'TSTypeAliasDeclaration',
],
},
],
'tsdoc/syntax': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-interface': 'warn',
'import/no-restricted-paths': [
'error',
{
zones: [
{
target: './src/background',
from: './src/views',
message:
'You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!',
},
{
target: './src/views',
from: './src/background',
message:
'You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!',
},
{
target: './src/shared',
from: './',
except: ['./src/shared', './node_modules'],
message: 'You cannot import into `shared` from an external directory.',
},
],
},
],
'import/extensions': 'off',
'no-restricted-syntax': [
'error',
'ForInStatement',
'LabeledStatement',
'WithStatement',
{
selector: 'TSEnumDeclaration',
message: "Don't declare enums",
},
],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import-essentials/restrict-import-depth': 'error',
'import-essentials/check-path-alias': 'error',
},
};

View File

@@ -1,9 +1,27 @@
## [2.2.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.1...v2.2.2) (2025-10-13)
### Features
* add nix flake ([#593](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/593)) ([7b401ad](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7b401add1565ff401bad99745ff9e53b9a7f899f))
* automatically select new or duplicated schedules ([#583](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/583)) ([#589](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/589)) ([2a50f55](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2a50f5580d3dbeb0d66546c23cf29bbb37d80da2))
* **env:** add SENTRY env vars ([8f7e1bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8f7e1bc0af6336549068e02b80df21d4e8f4ef9c))
* export schedule button add to calendar ([#594](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/594)) ([5994ded](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5994ded8be876cb55174d27d3fdb0832b21a0ff9))
* search result shading ([#617](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/617)) ([be861b8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be861b823cb2cb7f6f4a1f266351eec3fc1c2f99))
* show warning for courses of different semesters ([#570](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/570)) ([2e7dac1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2e7dac1e3eba757231ac07ac966231c08c703a16))
* support summer grades, fix summer course parser ([#596](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/596)) ([2d92dd4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d92dd47f00a44b7d48e92a8ffba94480e4e73f9))
### Bug Fixes
* fix or ignore various eslint warning ([#609](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/609)) ([95de8df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/95de8df37243b6d59625df515a60442f11b7a9d3))
* limit height of schedule list dropdown in the extension popup ([#543](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/543)) ([eb8141e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/eb8141ee8c3d32bce901457178d50781b78f86dd))
* whitespace wrapping in semester warning ([#629](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/629)) ([46fe591](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/46fe591fa72ef017eea7cfb8aa37d12d8f223926))
## [2.2.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.0...v2.2.1) (2025-06-04)
### Features
* add dining app promo ([#598](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/598)) ([be1dccf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be1dccfcb9d052c6b291b50cc53418d6bb645beb))
* inside jokes005 ([#590](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/590)) ([37471ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/37471efb740c7a5828cf3b54bac70954694359d7))
* **release:** v2.2.1 ([234f3d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/234f3d627d603adf8555b4d0e93106d198918169))
### Bug Fixes

32
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,32 @@
# UT Registration Plus Code of Conduct
Like the technical community as a whole, the UT Registration Plus team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people.
Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance.
This isnt an exhaustive list of things that you cant do. Rather, take it in the spirit in which its intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
This code of conduct applies to all spaces managed by the UT Registration Plus project or Longhorn Developers. This includes IRC, the mailing lists, the issue tracker, DSF events, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
If you believe someone is violating the code of conduct, we ask that you report it by emailing [contact@longhorns.dev](mailto:contact@longhorns.dev). For more details please see our
- **Be friendly and patient.**
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Its important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the UT Registration Plus community should be respectful when dealing with other members as well as with people outside the UT Registration Plus community.
- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
- Violent threats or language directed against another person.
- Discriminatory jokes and language.
- Posting sexually explicit or violent material.
- Posting (or threatening to post) other people's personally identifying information ("doxing").
- Personal insults, especially those using racist or sexist terms.
- Unwelcome sexual attention.
- Advocating for, or encouraging, any of the above behavior.
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and UT Registration Plus is no exception. It is important that we resolve disagreements and differing views constructively. Remember that were different. The strength of UT Registration Plus comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
## Questions?
If you have questions, please see . If that doesn't answer your questions, feel free to [contact us](mailto:contact@longhorns.dev).

257
eslint.config.mjs Normal file
View File

@@ -0,0 +1,257 @@
import { fixupConfigRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import importEssentials from 'eslint-plugin-import-essentials';
import jsdoc from 'eslint-plugin-jsdoc';
import reactPreferFunction from 'eslint-plugin-react-prefer-function-component';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import tsdoc from 'eslint-plugin-tsdoc';
import unocss from '@unocss/eslint-config/flat';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['*.html', 'tsconfig.json', 'dist/**', 'build/**', 'node_modules/**'],
},
js.configs.recommended,
...fixupConfigRules(
compat.extends(
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
'airbnb-base',
'airbnb/rules/react',
'airbnb-typescript',
'prettier'
)
),
unocss,
{
plugins: {
'import-essentials': importEssentials,
jsdoc,
tsdoc,
'react-prefer-function-component': reactPreferFunction,
'simple-import-sort': simpleImportSort,
},
languageOptions: {
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
debugger: true,
browser: true,
context: true,
JSX: true,
},
ecmaVersion: 2022,
sourceType: 'module',
parserOptions: {
project: './tsconfig.json',
ecmaFeatures: {
jsx: true,
modules: true,
experimentalObjectRestSpread: true,
},
},
},
settings: {
react: {
version: 'detect',
},
jsdoc: {
mode: 'typescript',
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
},
},
rules: {
// Disable rules removed in @typescript-eslint v8
'@typescript-eslint/no-throw-literal': 'off',
'prefer-const': [
'off',
{
destructuring: 'any',
ignoreReadBeforeAssign: false,
},
],
'no-plusplus': 'off',
'no-inner-declarations': 'off',
'sort-imports': 'off',
'no-case-declarations': 'off',
'no-unreachable': 'warn',
'no-constant-condition': 'error',
'space-before-function-paren': 'off',
'no-undef': 'off',
'no-return-await': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/no-shadow': ['off'],
'@typescript-eslint/no-use-before-define': ['off'],
'class-methods-use-this': 'off',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/lines-between-class-members': 'off',
'no-param-reassign': [
'error',
{
props: false,
},
],
'no-console': 'off',
'consistent-return': 'off',
'react/destructuring-assignment': 'off',
'import/prefer-default-export': 'off',
'no-promise-executor-return': 'off',
'import/no-cycle': 'off',
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'react/jsx-no-useless-fragment': [
'error',
{
allowExpressions: true,
},
],
'keyword-spacing': [
'error',
{
before: true,
after: true,
},
],
'no-continue': 'off',
'space-before-blocks': [
'error',
{
functions: 'always',
keywords: 'always',
classes: 'always',
},
],
'react/jsx-filename-extension': [
1,
{
extensions: ['.tsx'],
},
],
'react/no-deprecated': 'warn',
'react/prop-types': 'off',
'react-prefer-function-component/react-prefer-function-component': [
'warn',
{
allowComponentDidCatch: false,
},
],
'react/function-component-definition': 'off',
'react/button-has-type': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-returns-type': 'off',
'jsdoc/newline-after-description': 'off',
'react/require-default-props': 'off',
'jsdoc/require-jsdoc': [
'error',
{
enableFixer: false,
publicOnly: true,
checkConstructors: false,
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionExpression: true,
},
contexts: [
'MethodDefinition:not([key.name="componentDidMount"]):not([key.name="render"])',
'ArrowFunctionExpression',
'ClassDeclaration',
'ClassExpression',
'ClassProperty:not([key.name="state"]):not([key.name="componentDidMount"])',
'FunctionDeclaration',
'FunctionExpression',
'TSDeclareFunction',
'TSEnumDeclaration',
'TSInterfaceDeclaration',
'TSMethodSignature',
'TSModuleDeclaration',
'TSTypeAliasDeclaration',
],
},
],
'tsdoc/syntax': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-interface': 'warn',
'import/no-restricted-paths': [
'error',
{
zones: [
{
target: './src/background',
from: './src/views',
message:
'You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!',
},
{
target: './src/views',
from: './src/background',
message:
'You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!',
},
{
target: './src/shared',
from: './',
except: ['./src/shared', './node_modules'],
message: 'You cannot import into `shared` from an external directory.',
},
],
},
],
'import/extensions': 'off',
'no-restricted-syntax': [
'error',
'ForInStatement',
'LabeledStatement',
'WithStatement',
{
selector: 'TSEnumDeclaration',
message: "Don't declare enums",
},
],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import-essentials/restrict-import-depth': 'error',
'import-essentials/check-path-alias': 'error',
},
},
];

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1744932701,
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
"lastModified": 1759831965,
"narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
"rev": "c9b6fb798541223bbb396d287d16f43520250518",
"type": "github"
},
"original": {

View File

@@ -1,31 +1,42 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
inputs:
inputs.flake-utils.lib.eachDefaultSystem (
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = (import (inputs.nixpkgs) { inherit system; });
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;
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20 # v20.19.0
pnpm_10 # v10.8.1
just
];
devShells.default = pkgs.mkShell {
name = "utrp-dev";
buildInputs = commonPackages;
};
shellHook = ''
echo "UTRP Nix Flake Environment Loaded"
echo "Node: $(node --version)"
echo "pnpm: $(pnpm --version)"
'';
devShells.full = pkgs.mkShell {
name = "utrp-dev-full";
buildInputs = commonPackages ++ additionalPackages;
};
}
);

View File

@@ -1,7 +1,7 @@
{
"name": "ut-registration-plus",
"displayName": "UT Registration Plus",
"version": "2.2.1",
"version": "2.2.2",
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
"private": true,
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
@@ -15,8 +15,8 @@
"zip:to-publish": "SENTRY_ENV='production' pnpm zip",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
"lint": "eslint src --report-unused-disable-directives",
"lint:fix": "eslint src --report-unused-disable-directives --fix",
"check-types": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui",
@@ -67,6 +67,9 @@
"@commitlint/config-conventional": "^19.7.1",
"@commitlint/types": "^19.5.0",
"@crxjs/vite-plugin": "2.0.0-beta.21",
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.1",
"@iconify-json/bi": "^1.2.2",
"@iconify-json/ic": "^1.2.2",
"@iconify-json/iconoir": "^1.2.7",
@@ -96,8 +99,8 @@
"@types/semantic-release": "^20.0.6",
"@types/semver": "^7.5.8",
"@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"@unocss/eslint-config": "^0.63.6",
"@unocss/postcss": "^0.63.6",
"@unocss/preset-uno": "^0.63.6",
@@ -114,29 +117,29 @@
"cssnano-preset-advanced": "^7.0.6",
"dotenv": "^16.4.7",
"es-module-lexer": "^1.6.0",
"eslint": "^8.57.1",
"eslint": "^9.39.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.8.3",
"eslint-plugin-import": "^2.31.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-import-essentials": "^0.2.1",
"eslint-plugin-jsdoc": "^50.6.3",
"eslint-plugin-jsdoc": "^61.2.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-prefer-function-component": "^3.4.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^0.9.0",
"eslint-plugin-storybook": "^10.0.8",
"eslint-plugin-tsdoc": "^0.3.0",
"gulp": "^5.0.0",
"gulp-execa": "^7.0.1",
"gulp-zip": "^6.1.0",
"path": "^0.12.7",
"postcss": "^8.5.3",
"prettier": "^3.5.2",
"prettier": "^3.6.2",
"react-dev-utils": "^12.0.1",
"semantic-release": "^24.2.3",
"storybook": "^8.6.0",
@@ -144,7 +147,7 @@
"unocss": "^0.63.6",
"unocss-preset-primitives": "0.0.2-beta.1",
"unplugin-icons": "^0.19.3",
"vite": "^5.4.14",
"vite": "^5.4.20",
"vite-plugin-inspect": "^0.8.9",
"vitest": "^2.1.9"
},
@@ -162,7 +165,7 @@
}
},
"volta": {
"node": "20.9.0",
"pnpm": "10.6.5"
"node": "20.19.4",
"pnpm": "10.14.0"
}
}

2494
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import CourseCatalogMain from '@views/components/CourseCatalogMain';
import InjectedButton from '@views/components/injected/AddAllButton';
import DaysCheckbox from '@views/components/injected/DaysCheckbox';
import ShadedResults from '@views/components/injected/SearchResultShader';
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
import React from 'react';
import { createRoot } from 'react-dom/client';
@@ -30,3 +31,7 @@ if (support === SiteSupport.MY_UT) {
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
renderComponent(DaysCheckbox);
}
if (support === SiteSupport.COURSE_CATALOG_KWS) {
renderComponent(ShadedResults);
}

View File

@@ -24,6 +24,8 @@ export interface IOptionsStore {
/** whether the promo should be shown */
showUTDiningPromo: boolean;
/** whether users are allowed to bypass the 10 schedule limit */
allowMoreSchedules: boolean;
}
export const OptionsStore = createSyncStore<IOptionsStore>({
@@ -34,6 +36,7 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
alwaysOpenCalendarInNewTab: false,
showCalendarSidebar: true,
showUTDiningPromo: true,
allowMoreSchedules: false,
});
/**
@@ -50,6 +53,7 @@ export const initSettings = async () =>
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
allowMoreSchedules: await OptionsStore.get('allowMoreSchedules'),
}) satisfies IOptionsStore;
// Clothing retailer right

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import { CalendarDots, Export, FilePng, Sidebar } from '@phosphor-icons/react';
import { CalendarDots, Export, FileCode, FilePng, Sidebar } from '@phosphor-icons/react';
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
import { Button } from '@views/components/common/Button';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
@@ -11,7 +11,7 @@ import useSchedules from '@views/hooks/useSchedules';
import clsx from 'clsx';
import React from 'react';
import { saveAsCal, saveCalAsPng } from '../utils';
import { handleExportJson, saveAsCal, saveCalAsPng } from '../utils';
interface CalendarHeaderProps {
sidebarOpen?: boolean;
@@ -98,6 +98,18 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
Save as .cal
</Button>
</MenuItem>
<MenuItem>
<Button
className='w-full flex justify-start'
onClick={() => handleExportJson(activeSchedule.id)}
color='ut-black'
size='small'
variant='minimal'
icon={FileCode}
>
Save as .json
</Button>
</MenuItem>
{/* <MenuItem>
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
Export Unique IDs

View File

@@ -1,5 +1,5 @@
import { AppStoreLogo, ForkKnife, X as CloseIcon } from '@phosphor-icons/react';
import { UT_DINING_APP_STORE_URL, UT_DINING_GOOGLE_PLAY_URL } from '@shared/util/appUrls';
import { 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';

View File

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

View File

@@ -1,4 +1,5 @@
import { tz, TZDate } from '@date-fns/tz';
import exportSchedule from '@pages/background/lib/exportSchedule';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Course } from '@shared/types/Course';
import type { CourseMeeting } from '@shared/types/CourseMeeting';
@@ -261,6 +262,22 @@ export const saveAsCal = async () => {
downloadBlob(icsString, 'CALENDAR', 'schedule.ics');
};
/**
* Saves current schedule to JSON that can be imported on other devices.
* @param id - Provided schedule ID to download
*/
export const handleExportJson = async (id: string) => {
const jsonString = await exportSchedule(id);
if (jsonString) {
const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(s => s.id === id);
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
await downloadBlob(jsonString, 'JSON', fileName);
} else {
console.error('Error exporting schedule: jsonString is undefined');
}
};
/**
* Saves the calendar as a PNG image.
*

View File

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

View File

@@ -4,7 +4,7 @@ import type { ThemeColor } from '@shared/types/ThemeColors';
import { getThemeColorHexByName, getThemeColorRgbByName } from '@shared/util/themeColors';
import Text from '@views/components/common/Text/Text';
import clsx from 'clsx';
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
interface Props {
className?: string;
@@ -21,9 +21,8 @@ interface Props {
}
/**
* A reusable input button component that follows the Button.tsx consistency
*
* @returns
* A reusable input button component that follows Button.tsx consistency.
* Now supports drag-and-drop file uploads (issue #446).
*/
export default function FileUpload({
className,
@@ -43,22 +42,74 @@ export default function FileUpload({
const isIconOnly = !children && !!icon;
const colorHex = getThemeColorHexByName(color);
const colorRgb = getThemeColorRgbByName(color)?.join(' ');
const inputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
// Convert accept to a comma-separated string if it's an array
// Convert accept array to comma-separated list
const acceptValue = Array.isArray(accept) ? accept.join(',') : accept;
// --- Prevent Chrome from opening the file on drop anywhere else ---
useEffect(() => {
const preventDefault = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
window.addEventListener('dragover', preventDefault);
window.addEventListener('drop', preventDefault);
return () => {
window.removeEventListener('dragover', preventDefault);
window.removeEventListener('drop', preventDefault);
};
}, []);
// ------------------------------------------------------------------
// --- Local drag and drop handlers for this button only -------------
const handleDrop = (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
if (disabled) return;
const file = event.dataTransfer.files?.[0];
if (file && inputRef.current && onChange) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
inputRef.current.files = dataTransfer.files;
// Trigger change event manually
onChange({ target: inputRef.current } as React.ChangeEvent<HTMLInputElement>);
}
};
const handleDragOver = (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault();
event.stopPropagation();
if (!disabled) setIsDragging(true);
};
const handleDragLeave = (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
};
// ------------------------------------------------------------------
return (
<label
style={
{
...style,
color: disabled ? 'ut-gray' : colorHex,
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity)`,
} satisfies React.CSSProperties
}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
style={{
...style,
color: disabled ? 'ut-gray' : colorHex,
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity))`,
}}
className={clsx(
'btn',
'btn transition-colors select-none',
{
'ring-2 ring-offset-2 ring-blue-400': isDragging && !disabled,
'text-white! bg-opacity-100 hover:enabled:shadow-md active:enabled:shadow-sm shadow-black/20':
variant === 'filled',
'bg-opacity-0 border-current hover:enabled:bg-opacity-8 border stroke-width-[1px]':
@@ -70,6 +121,7 @@ export default function FileUpload({
'h-[35px] w-[35px] p-spacing-2': size === 'small' && isIconOnly,
'h-6 p-spacing-2': size === 'mini' && !isIconOnly,
'h-6 w-6 p-0': size === 'mini' && isIconOnly,
'opacity-60 cursor-not-allowed': disabled,
},
className
)}
@@ -85,6 +137,7 @@ export default function FileUpload({
</Text>
)}
<input
ref={inputRef}
type='file'
{...(accept ? { accept: acceptValue } : {})}
className='hidden'

View File

@@ -15,7 +15,7 @@ import React, { useEffect, useState } from 'react';
*/
const WHATSNEW_POPUP_VERSION = 2;
const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
// const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
type Feature = {
id: string;
@@ -60,7 +60,7 @@ const NEW_FEATURES = [
* @returns A JSX of WhatsNewPopupContent component.
*/
export default function WhatsNewPopupContent(): JSX.Element {
const [videoError, setVideoError] = useState(false);
const [videoError, _setVideoError] = useState(false);
return (
<div className='w-full flex flex-row justify-between'>

View File

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

View File

@@ -1,62 +0,0 @@
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
function getCourseSections() {
const table = document.querySelector('table');
if (!table) return [];
const rows = Array.from(table.querySelectorAll('tr'));
const sections: { header: HTMLTableRowElement; children: HTMLTableRowElement[] }[] = [];
let currentSection: { header: HTMLTableRowElement; children: HTMLTableRowElement[] } | null = null;
for (const row of rows) {
const headerCell = row.querySelector('td.course_header');
if (headerCell) {
if (currentSection) sections.push(currentSection);
currentSection = { header: row, children: [] };
} else if (currentSection) {
currentSection.children.push(row);
}
}
if (currentSection) sections.push(currentSection);
return sections;
}
const CollapsibleSection: React.FC<{
header: HTMLTableRowElement;
childrenRows: HTMLTableRowElement[];
}> = ({ header, childrenRows }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
// Hide children rows initially
childrenRows.forEach(row => (row.style.display = open ? '' : 'none'));
// Clean up on unmount
return () => {
childrenRows.forEach(row => (row.style.display = ''));
};
}, [open, childrenRows]);
// Inject a button into the header cell
useEffect(() => {
const cell = header.querySelector('td.course_header');
if (!cell) return;
let button = cell.querySelector('.utrp-collapse-btn') as HTMLButtonElement | null;
if (!button) {
button = document.createElement('button');
button.className = 'utrp-collapse-btn';
button.style.marginRight = '8px';
cell.prepend(button);
}
button.textContent = open ? '▼' : '►';
button.onclick = () => setOpen(o => !o);
// Clean up
return () => {
button?.remove();
};
}, [header, open]);
return null;
};
export default

View File

@@ -1,3 +1,5 @@
import createSchedule from '@pages/background/lib/createSchedule';
import switchSchedule from '@pages/background/lib/switchSchedule';
import {
ArrowUpRight,
CalendarDots,
@@ -14,8 +16,10 @@ import { background } from '@shared/messages';
import type { Course } from '@shared/types/Course';
import type Instructor from '@shared/types/Instructor';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { englishStringifyList } from '@shared/util/string';
import { Button } from '@views/components/common/Button';
import { Chip, coreMap, flagMap } from '@views/components/common/Chip';
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
import Divider from '@views/components/common/Divider';
import Link from '@views/components/common/Link';
import Text from '@views/components/common/Text/Text';
@@ -60,7 +64,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
const [isCopied, setIsCopied] = useState<boolean>(false);
const lastCopyTime = useRef<number>(0);
const showDialog = usePrompt();
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
@@ -112,10 +116,78 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
}
};
const handleAddToNewSchedule = async (close: () => void) => {
const newScheduleId = await createSchedule(`${course.semester.season} ${course.semester.year}`);
switchSchedule(newScheduleId);
addCourse({ course, scheduleId: newScheduleId });
close();
};
const handleAddOrRemoveCourse = async () => {
const uniqueSemesterCodes = [
...new Set(
activeSchedule.courses
.map(course => course.semester.code)
.filter((code): code is string => code !== undefined)
),
];
uniqueSemesterCodes.sort();
const codeToReadableMap: Record<string, string> = {};
activeSchedule.courses.forEach(course => {
const { code } = course.semester;
if (code) {
const readable = `${course.semester.season} ${course.semester.year}`;
codeToReadableMap[code] = readable;
}
});
const sortedSemesters = uniqueSemesterCodes
.map(code => codeToReadableMap[code])
.filter((value): value is string => value !== undefined);
const activeSemesters = englishStringifyList(sortedSemesters);
if (!activeSchedule) return;
if (!courseAdded) {
addCourse({ course, scheduleId: activeSchedule.id });
const currentSemesterCode = course.semester.code;
// Show warning if this course is for a different semester than the selected schedule
if (
activeSchedule.courses.length > 0 &&
activeSchedule.courses.every(otherCourse => otherCourse.semester.code !== currentSemesterCode)
) {
const dialogButtons = (close: () => void) => (
<>
<Button variant='minimal' color='ut-black' onClick={close}>
Cancel
</Button>
<Button
variant='filled'
color='ut-burntorange'
onClick={() => {
handleAddToNewSchedule(close);
}}
>
Start a new schedule
</Button>
</>
);
showDialog({
title: 'This course section is from a different semester!',
description: (
<>
The section you&apos;re adding is for{' '}
<span className='whitespace-nowrap text-ut-burntorange'>
{course.semester.season} {course.semester.year}
</span>
, but your current schedule contains sections in{' '}
<span className='whitespace-nowrap text-ut-burntorange'>{activeSemesters}</span>. Mixing
semesters in one schedule may cause confusion.
</>
),
buttons: dialogButtons,
});
} else {
addCourse({ course, scheduleId: activeSchedule.id });
}
} else {
removeCourse({ course, scheduleId: activeSchedule.id });
}

View File

@@ -0,0 +1,39 @@
import { useEffect } from 'react';
// @TODO Get a better name for this class
/**
* The existing search results (kws), only with alternate shading for easier readability
*
*/
export default function ShadedResults(): null {
useEffect(() => {
const table = document.getElementById('kw_results_table');
if (!table) {
console.error('Results table not found');
return;
}
const tbody = table.querySelector('tbody');
if (!tbody) {
console.error('Table tbody not found');
return;
}
const style = document.createElement('style');
style.textContent = `
#kw_results_table tbody tr:nth-child(even) {
background-color: #f0f0f0 !important;
}
#kw_results_table tbody tr:nth-child(even) td {
background-color: #f0f0f0 !important;
}
`;
document.head.appendChild(style);
return () => {
style.remove();
};
}, []);
return null;
}

View File

@@ -1,16 +1,13 @@
// import addCourse from '@pages/background/lib/addCourse';
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
import exportSchedule from '@pages/background/lib/exportSchedule';
import importSchedule from '@pages/background/lib/importSchedule';
import { CalendarDots, Trash } from '@phosphor-icons/react';
import { background } from '@shared/messages';
import { DevStore } from '@shared/storage/DevStore';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { CRX_PAGES } from '@shared/types/CRXPages';
import MIMEType from '@shared/types/MIMEType';
import { downloadBlob } from '@shared/util/downloadBlob';
// import { addCourseByUrl } from '@shared/util/courseUtils';
// import { getCourseColors } from '@shared/util/colors';
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
@@ -32,6 +29,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import IconoirGitFork from '~icons/iconoir/git-fork';
import { handleExportJson } from '../calendar/utils';
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
import FileUpload from '../common/FileUpload';
import { useMigrationDialog } from '../common/MigrationDialog';
@@ -94,6 +92,7 @@ export default function Settings(): JSX.Element {
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
const [calendarNewTab, setCalendarNewTab] = useState<boolean>(false);
const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState<boolean>(false);
const showMigrationDialog = useMigrationDialog();
@@ -128,6 +127,7 @@ export default function Settings(): JSX.Element {
enableScrollToLoad,
enableDataRefreshing,
alwaysOpenCalendarInNewTab,
allowMoreSchedules,
} = await initSettings();
setEnableCourseStatusChips(enableCourseStatusChips);
// setShowTimeLocation(enableTimeAndLocationInPopup);
@@ -135,6 +135,7 @@ export default function Settings(): JSX.Element {
setLoadAllCourses(enableScrollToLoad);
setEnableDataRefreshing(enableDataRefreshing);
setCalendarNewTab(alwaysOpenCalendarInNewTab);
setIncreaseScheduleLimit(allowMoreSchedules);
};
const initDS = async () => {
@@ -189,6 +190,15 @@ export default function Settings(): JSX.Element {
// console.log('alwaysOpenCalendarInNewTab', newValue);
});
const l6 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
setCalendarNewTab(newValue);
// console.log('alwaysOpenCalendarInNewTab', newValue);
});
const l7 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
setIncreaseScheduleLimit(newValue);
});
// Remove listeners when the component is unmounted
return () => {
OptionsStore.removeListener(l1);
@@ -196,6 +206,8 @@ export default function Settings(): JSX.Element {
OptionsStore.removeListener(l3);
OptionsStore.removeListener(l4);
OptionsStore.removeListener(l5);
OptionsStore.removeListener(l6);
OptionsStore.removeListener(l7);
DevStore.removeListener(ds_l1);
@@ -232,35 +244,20 @@ export default function Settings(): JSX.Element {
});
};
const handleExportClick = async (id: string) => {
const jsonString = await exportSchedule(id);
if (jsonString) {
const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(s => s.id === id);
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
await downloadBlob(jsonString, 'JSON', fileName);
} else {
console.error('Error exporting schedule: jsonString is undefined');
}
};
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async e => {
try {
const result = e.target?.result as string;
const jsonObject = JSON.parse(result);
await importSchedule(jsonObject);
} catch (error) {
console.error('Invalid import file!');
}
};
reader.readAsText(file);
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
await importSchedule(data);
alert('Schedule imported successfully.');
} catch (error) {
console.error('Error importing schedule:', error);
alert('Failed to import schedule. Make sure the file is a valid .json format.');
}
};
// const handleAddCourseByLink = async () => {
// // todo: Use a proper modal instead of a prompt
// const link: string | null = prompt('Enter course link');
@@ -400,7 +397,7 @@ export default function Settings(): JSX.Element {
<Button
variant='outline'
color='ut-burntorange'
onClick={() => handleExportClick(activeSchedule.id)}
onClick={() => handleExportJson(activeSchedule.id)}
>
Export
</Button>
@@ -466,6 +463,25 @@ export default function Settings(): JSX.Element {
/>
</div>
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Allow more than 10 schedules
</Text>
<p className='text-sm text-gray-600'>
Allow bypassing the 10-schedule limit. Intended for advisors or staff who
need to create many schedules on behalf of students.
</p>
</div>
<SwitchButton
isChecked={increaseScheduleLimit}
onChange={() => {
setIncreaseScheduleLimit(!increaseScheduleLimit);
OptionsStore.set('allowMoreSchedules', !increaseScheduleLimit);
}}
/>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>

View File

@@ -1,5 +1,8 @@
import { background } from '@shared/messages';
import { OptionsStore } from '@shared/storage/OptionsStore';
import { CRX_PAGES } from '@shared/types/CRXPages';
import useSchedules from '@views/hooks/useSchedules';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Button } from '../components/common/Button';
import { usePrompt } from '../components/common/DialogProvider/DialogProvider';
@@ -17,8 +20,33 @@ const SCHEDULE_LIMIT = 10;
export function useEnforceScheduleLimit(): () => boolean {
const [, schedules] = useSchedules();
const showDialog = usePrompt();
const [allowMoreSchedules, setAllowMoreSchedules] = useState<boolean>(false);
useEffect(() => {
let mounted = true;
(async () => {
try {
const val = await OptionsStore.get('allowMoreSchedules');
if (mounted) setAllowMoreSchedules(val ?? false);
} catch (err) {
console.error('Failed to read allowMoreSchedules from OptionsStore:', err);
}
})();
const listener = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
setAllowMoreSchedules(newValue);
});
return () => {
mounted = false;
OptionsStore.removeListener(listener);
};
}, []);
return useCallback(() => {
// If user has enabled bypass, allow creating more schedules
if (allowMoreSchedules) return true;
if (schedules.length >= SCHEDULE_LIMIT) {
showDialog({
title: `You have too many schedules!`,
@@ -27,19 +55,33 @@ export function useEnforceScheduleLimit(): () => boolean {
<>
To encourage organization,{' '}
<span className='text-ut-burntorange'>please consider deleting any unused schedules</span> you
may have.
may have. You can increase the limit in the settings if its really necessary.
</>
),
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
I understand
</Button>
<>
<Button
variant='outline'
color='ut-black'
onClick={() => {
close();
// open options/settings page so user can enable bypass if they are advising staff
const url = chrome.runtime.getURL(CRX_PAGES.OPTIONS);
background.openNewTab({ url });
}}
>
Open Settings
</Button>
<Button variant='filled' color='ut-burntorange' onClick={close}>
I understand
</Button>
</>
),
});
return false;
}
return true;
}, [schedules, showDialog]);
}, [schedules, showDialog, allowMoreSchedules]);
}

View File

@@ -5,8 +5,7 @@ import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
import { useDialog } from '@views/contexts/DialogContext';
import React from 'react';
import { LogoIcon } from '../components/common/LogoIcon';
import useChangelog from './useChangelog';
// import useChangelog from './useChangelog';
const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
@@ -17,8 +16,8 @@ const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
*/
export default function useWhatsNewPopUp(): () => void {
const showDialog = useDialog();
const showChangeLog = useChangelog();
const { version } = chrome.runtime.getManifest();
// const showChangeLog = useChangelog();
// const { version } = chrome.runtime.getManifest();
const showPopUp = () => {
showDialog(close => ({

View File

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