Compare commits

..

3 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
51 changed files with 2100 additions and 2529 deletions

View File

@@ -8,5 +8,5 @@ trim_trailing_whitespace = true
indent_size = 4 indent_size = 4
indent_style = space indent_style = space
[*.{nix,yaml,yml}] [*.nix]
indent_size = 2 indent_size = 2

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,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: '' labels: ''
assignees: '' assignees: ''
--- ---
**Pre-submission Checklist** **Pre-submission Checklist**
@@ -29,11 +30,11 @@ assignees: ''
**Expected Behavior** **Expected Behavior**
<!-- What you expected to happen --> <!-- A clear and concise description of what you expected to happen -->
**Current Behavior** **Current Behavior**
<!-- What actually happened --> <!-- A clear and concise description of what actually happened -->
**Screenshots** **Screenshots**

View File

@@ -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**
@@ -16,14 +17,18 @@ assignees: ''
- [ ] I have reviewed the documentation to confirm this feature doesn't exist - [ ] I have reviewed the documentation to confirm this feature doesn't exist
- [ ] I have completed all sections below with detailed information - [ ] I have completed all sections below with detailed information
**Your Idea** **Feature Description**
<!-- A clear and concise description of the feature you'd like to see, and how it would work --> <!-- A clear and concise description of the feature you'd like to see -->
**Proposed Solution**
<!-- A clear and concise description of what you want to happen -->
**UI/UX Considerations** **UI/UX Considerations**
<!-- If this feature involves UI changes (aka how it looks), please describe the visual aspects --> <!-- If this feature involves UI changes, please describe the visual aspects -->
**Other** **Technical Implementation Details**
<!-- Any other comments you have can go here! --> <!-- If you have specific technical suggestions, list them here -->

View File

@@ -4,6 +4,7 @@ about: Updating Build Dependencies
title: '' title: ''
labels: build, dependencies labels: build, dependencies
assignees: doprz, Razboy20 assignees: doprz, Razboy20
--- ---
- [ ] Updated Nix Flake - [ ] Updated Nix Flake

View File

@@ -15,6 +15,7 @@ updates:
major-updates: major-updates:
update-types: update-types:
- 'major' - 'major'
ignore: ignore:
- dependency-name: '@crxjs/vite-plugin' - dependency-name: '@crxjs/vite-plugin'
- dependency-name: '@unocss/vite' - dependency-name: '@unocss/vite'

View File

@@ -1,33 +1,43 @@
name: Best Practices name: Best Practices
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Run ESLint - name: Run ESLint
run: pnpm run lint run: pnpm run lint
format: format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Run Prettier - name: Run Prettier
run: pnpm run prettier run: pnpm run prettier

View File

@@ -1,18 +1,24 @@
name: Type Check name: Type Check
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
type-check: type-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Run tests - name: Run tests
run: pnpm run check-types run: pnpm run check-types

View File

@@ -1,5 +1,7 @@
name: 'Chromatic' name: 'Chromatic'
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
chromatic: chromatic:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -12,8 +14,10 @@ jobs:
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Publish to Chromatic - name: Publish to Chromatic
uses: chromaui/action@latest uses: chromaui/action@latest
with: with:

View File

@@ -15,6 +15,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Get file permission - name: Get file permission
run: chmod -R 777 . run: chmod -R 777 .
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Release with semantic-release - name: Release with semantic-release

View File

@@ -1,18 +1,24 @@
name: Tests name: Tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Run tests - name: Run tests
run: pnpm test run: pnpm test

View File

@@ -1,6 +1,8 @@
name: Validate PR Title name: Validate PR Title
# thank you ben limmer for this workflow: # thank you ben limmer for this workflow:
# https://github.com/blimmer/semantic-release-demo-2/blob/main/.github/workflows/lint-pr.yml # https://github.com/blimmer/semantic-release-demo-2/blob/main/.github/workflows/lint-pr.yml
on: on:
pull_request_target: pull_request_target:
types: types:
@@ -8,6 +10,7 @@ on:
- reopened - reopened
- edited - edited
- synchronize - synchronize
jobs: jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest

7
.gitignore vendored
View File

@@ -212,9 +212,4 @@ package-lock.json
storybook-static/ storybook-static/
package/ package/
# Nix .direnv/
result
result-*
# direnv
.direnv

8
.vscode/launch.json vendored
View File

@@ -6,9 +6,13 @@
"request": "launch", "request": "launch",
"name": "Run current script", "name": "Run current script",
"runtimeExecutable": "npx", "runtimeExecutable": "npx",
"runtimeArgs": ["tsx"], "runtimeArgs": [
"tsx"
],
"program": "${file}", "program": "${file}",
"skipFiles": ["<node_internals>/**"] "skipFiles": [
"<node_internals>/**"
],
} }
] ]
} }

View File

@@ -26,7 +26,7 @@
"navigation": "Routes", "navigation": "Routes",
"logging": "log", "logging": "log",
"popup": "Layout", "popup": "Layout",
"storage": "Database" "storage": "Database",
}, },
"material-icon-theme.files.associations": { "material-icon-theme.files.associations": {
"tsconfig.extension.json": "tsconfig", "tsconfig.extension.json": "tsconfig",
@@ -36,5 +36,5 @@
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
} }

View File

@@ -1,18 +1,3 @@
## [2.3.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.2...v2.3.0) (2026-01-07)
### Features
* add drag-and-drop import for schedules ([#661](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/661)) ([549c52a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/549c52a39fee718f2bb07cfce33a294835a2246b)), closes [#446](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/446)
* allow bypassing the 10-schedule limit ([#675](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/675)) ([6a67a32](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6a67a32e4f50a5bdd20aa43789f199b822483e2d))
* condense resourceLinks course schedule ([#676](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/676)) ([cee5f02](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cee5f0284f09f39ca5ae64559d0b697646c77e74))
* LHD birthday ([#717](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/717)) ([2d18553](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d18553f98c5146fa18699ae20462e7dcbc9d35c))
* **nix:** add prettier-version-match check ([#713](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/713)) ([8ccf7fb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8ccf7fb37e769ba445f39c140ca9c1c4245cc1c1))
* **nix:** build UTRP ([#714](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/714)) ([38bb29b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/38bb29b20b97ed3cf8fd6511df16553fed1d58bb))
### Bug Fixes
* .editorconfig syntax for nix files ([b406d4d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b406d4dd244a25688c2b9621cf5d441228bd8913))
* toSorted outdated chrome bug ([#694](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/694)) ([4f5d8c6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4f5d8c6d20e3cfeb7b62520ba1819e297d2cc60f))
## [2.2.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.1...v2.2.2) (2025-10-13) ## [2.2.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.1...v2.2.2) (2025-10-13)
### Features ### Features
@@ -21,7 +6,6 @@
* 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)) * 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)) * **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)) * 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)) * 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)) * 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)) * 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))

View File

@@ -205,7 +205,7 @@ Special thanks to the developers and contributors behind these amazing tools and
## Activity ## Activity
![UT-Registration-Plus Activity](https://repobeats.axiom.co/api/embed/47930fa3916ac1b475cd63a05948c449eb5ad502.svg 'UT-Registration-Plus Repobeats analytics image') ![UT-Registration-Plus Activity](https://repobeats.axiom.co/api/embed/47930fa3916ac1b475cd63a05948c449eb5ad502.svg "UT-Registration-Plus Repobeats analytics image")
## Star History ## Star History

View File

@@ -1,10 +0,0 @@
(import (
let
rev = "v1.1.0";
sha256 = "sha256:19d2z6xsvpxm184m41qrpi1bplilwipgnzv9jy17fgw421785q1m";
in
fetchTarball {
inherit sha256;
url = "https://github.com/NixOS/flake-compat/archive/${rev}.tar.gz";
}
) { src = ./.; }).defaultNix

View File

@@ -24,7 +24,7 @@ else
fi fi
# Validate the mode # Validate the mode
if [[ ! " ${SUPPORTED_MODES[*]} " =~ ${mode} ]]; then if [[ ! " ${SUPPORTED_MODES[*]} " =~ " ${mode} " ]]; then
echo "Error: Invalid mode '${mode}'" >&2 echo "Error: Invalid mode '${mode}'" >&2
usage usage
fi fi

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',
},
},
];

79
flake.lock generated
View File

@@ -1,30 +1,30 @@
{ {
"nodes": { "nodes": {
"flake-parts": { "flake-utils": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1767609335, "lastModified": 1731533236,
"narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "hercules-ci", "owner": "numtide",
"repo": "flake-parts", "repo": "flake-utils",
"rev": "250481aafeb741edfe23d29195671c19b36b6dca", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "hercules-ci", "owner": "numtide",
"repo": "flake-parts", "repo": "flake-utils",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1767640445, "lastModified": 1759831965,
"narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", "narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", "rev": "c9b6fb798541223bbb396d287d16f43520250518",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -34,59 +34,24 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"lastModified": 1765674936,
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1761236834,
"narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"treefmt-nix": "treefmt-nix"
} }
}, },
"treefmt-nix": { "systems": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": { "locked": {
"lastModified": 1767468822, "lastModified": 1681028828,
"narHash": "sha256-MpffQxHxmjVKMiQd0Tg2IM/bSjjdQAM+NDcX6yxj7rE=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "numtide", "owner": "nix-systems",
"repo": "treefmt-nix", "repo": "default",
"rev": "d56486eb9493ad9c4777c65932618e9c2d0468fc", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "nix-systems",
"repo": "treefmt-nix", "repo": "default",
"type": "github" "type": "github"
} }
} }

View File

@@ -1,33 +1,43 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-utils.url = "github:numtide/flake-utils";
treefmt-nix.url = "github:numtide/treefmt-nix";
}; };
outputs = outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = inputs.nixpkgs.lib.systems.flakeExposed;
imports = [
./nix/packages.nix
./nix/devShells.nix
./nix/treefmt.nix
];
perSystem =
{ system, ... }:
{ {
_module.args.pkgs = import inputs.nixpkgs { self,
inherit system; nixpkgs,
overlays = [ flake-utils,
(final: prev: { }:
nodejs = prev.nodejs_20; # v20.19.5 flake-utils.lib.eachDefaultSystem (
}) system:
let
pkgs = (import nixpkgs { inherit system; });
commonPackages = with pkgs; [
nodejs_20 # v20.19.5
pnpm_10 # v10.18.0
]; ];
config = { };
}; additionalPackages = with pkgs; [
bun
nodePackages.conventional-changelog-cli
sentry-cli
];
in
{
formatter = pkgs.nixfmt-rfc-style;
devShells.default = pkgs.mkShell {
name = "utrp-dev";
buildInputs = commonPackages;
}; };
devShells.full = pkgs.mkShell {
name = "utrp-dev-full";
buildInputs = commonPackages ++ additionalPackages;
}; };
} }
);
}

View File

@@ -1,30 +0,0 @@
{
perSystem =
{
pkgs,
...
}:
let
commonPackages = with pkgs; [
nodejs # Defined in overlay
pnpm_10 # v10.18.2
];
additionalPackages = with pkgs; [
bun
nodePackages.conventional-changelog-cli
sentry-cli
];
in
{
devShells.default = pkgs.mkShell {
name = "utrp-dev";
packages = commonPackages;
};
devShells.full = pkgs.mkShell {
name = "utrp-dev-full";
packages = commonPackages ++ additionalPackages;
};
};
}

View File

@@ -1,51 +0,0 @@
{
stdenv,
lib,
nodejs,
pnpm_10,
git,
version ? "dev",
gitRev ? "unknown",
gitBranch ? "unknown",
buildScript ? "build",
}:
stdenv.mkDerivation (finalAttrs: {
inherit version;
pname = "ut-registration-plus";
src = ../.;
nativeBuildInputs = [
nodejs
pnpm_10.configHook
git
];
pnpmDeps = pnpm_10.fetchDeps {
inherit (finalAttrs) pname version src;
fetcherVersion = 2;
hash = "sha256-UqHymJWvlTV4glra/6DkxuCxbG5dpPkFcnvq3vuxsJ8=";
};
# Pass git info to the build
VITE_GIT_COMMIT = gitRev;
VITE_GIT_BRANCH = gitBranch;
buildPhase = ''
pnpm run ${buildScript}
'';
installPhase = ''
mkdir -p $out
cp -r dist/* $out/
'';
meta = {
description = "UT Registration Plus";
homepage = "https://github.com/Longhorn-Developers/UT-Registration-Plus";
license = lib.licenses.mit;
maintainers = lib.maintainers.doprz;
platforms = lib.platforms.unix;
};
})

View File

@@ -1,40 +0,0 @@
{ inputs, ... }:
{
perSystem =
{ pkgs, ... }:
let
packageJson = builtins.fromJSON (builtins.readFile ../package.json);
gitRev = inputs.self.shortRev or inputs.self.dirtyShortRev or "dev";
gitBranch = if inputs.self ? ref then inputs.self.ref else "unknown";
baseVersion = packageJson.version;
commonArgs = {
inherit gitRev gitBranch;
};
# Prod variant
ut-registration-plus = pkgs.callPackage ./package.nix (
commonArgs
// {
version = "${baseVersion}+git.${gitRev}";
buildScript = "build";
}
);
# Dev variant
ut-registration-plus-dev = pkgs.callPackage ./package.nix (
commonArgs
// {
version = "${baseVersion}-dev+git.${gitRev}";
buildScript = "build:dev";
}
);
in
{
packages = {
inherit ut-registration-plus ut-registration-plus-dev;
default = ut-registration-plus;
dev = ut-registration-plus-dev;
};
};
}

View File

@@ -1,63 +0,0 @@
{ inputs, ... }:
{
imports = [
inputs.treefmt-nix.flakeModule
];
perSystem =
{ pkgs, ... }:
{
treefmt = {
projectRootFile = "flake.nix";
programs.nixfmt.enable = pkgs.lib.meta.availableOn pkgs.stdenv.buildPlatform pkgs.nixfmt-rfc-style.compiler;
programs.nixfmt.package = pkgs.nixfmt-rfc-style;
# NOTE: Make sure the prettier version in package.json and the one used by treefmt are the same for consistent results
programs.prettier.enable = true;
programs.shellcheck.enable = true;
programs.yamlfmt.enable = true;
programs.dockerfmt.enable = true;
settings.formatter.prettier.excludes = [ "pnpm-lock.yaml" ];
settings.formatter.shellcheck.excludes = [ ".envrc" ];
settings.formatter.yamlfmt.excludes = [ "pnpm-lock.yaml" ];
};
checks = {
prettier-version-match =
pkgs.runCommand "check-prettier-version"
{
buildInputs = [ pkgs.jq ];
}
''
# Extract prettier version from package.json
packageJsonVersion=$(jq -r '.devDependencies.prettier // empty' ${../package.json})
if [ -z "$packageJsonVersion" ]; then
echo "Error: prettier not found in package.json devDependencies"
exit 1
fi
# Remove any semver prefix characters (^, ~, etc...)
packageJsonVersion=$(echo "$packageJsonVersion" | sed 's/^[\^~>=<]*//')
# Get prettier version from nixpkgs
nixVersion="${pkgs.nodePackages.prettier.version}"
if [ "$packageJsonVersion" != "$nixVersion" ]; then
echo ""
echo "ERROR: Prettier version mismatch!"
echo " package.json: $packageJsonVersion"
echo " nixpkgs: $nixVersion"
echo ""
echo "Please update one of the following:"
echo " - Update prettier in package.json to match nixpkgs: $nixVersion"
echo " - Override prettier in your flake to match package.json"
exit 1
fi
touch $out
'';
};
};
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "ut-registration-plus", "name": "ut-registration-plus",
"displayName": "UT Registration Plus", "displayName": "UT Registration Plus",
"version": "2.3.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",
@@ -15,8 +15,8 @@
"zip:to-publish": "SENTRY_ENV='production' pnpm zip", "zip:to-publish": "SENTRY_ENV='production' pnpm zip",
"prettier": "prettier src --check", "prettier": "prettier src --check",
"prettier:fix": "prettier src --write", "prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives", "lint": "eslint src --report-unused-disable-directives",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --fix", "lint:fix": "eslint src --report-unused-disable-directives --fix",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
@@ -39,9 +39,6 @@
"@phosphor-icons/react": "^2.1.7", "@phosphor-icons/react": "^2.1.7",
"@sentry/react": "^8.55.0", "@sentry/react": "^8.55.0",
"@tanstack/react-query": "^5.69.0", "@tanstack/react-query": "^5.69.0",
"@tsparticles/engine": "^3.9.1",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.9.1",
"@unocss/vite": "^0.63.6", "@unocss/vite": "^0.63.6",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"chrome-extension-toolkit": "^0.0.54", "chrome-extension-toolkit": "^0.0.54",
@@ -70,6 +67,9 @@
"@commitlint/config-conventional": "^19.7.1", "@commitlint/config-conventional": "^19.7.1",
"@commitlint/types": "^19.5.0", "@commitlint/types": "^19.5.0",
"@crxjs/vite-plugin": "2.0.0-beta.21", "@crxjs/vite-plugin": "2.0.0-beta.21",
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.1",
"@iconify-json/bi": "^1.2.2", "@iconify-json/bi": "^1.2.2",
"@iconify-json/ic": "^1.2.2", "@iconify-json/ic": "^1.2.2",
"@iconify-json/iconoir": "^1.2.7", "@iconify-json/iconoir": "^1.2.7",
@@ -99,8 +99,8 @@
"@types/semantic-release": "^20.0.6", "@types/semantic-release": "^20.0.6",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^8.47.0",
"@unocss/eslint-config": "^0.63.6", "@unocss/eslint-config": "^0.63.6",
"@unocss/postcss": "^0.63.6", "@unocss/postcss": "^0.63.6",
"@unocss/preset-uno": "^0.63.6", "@unocss/preset-uno": "^0.63.6",
@@ -117,29 +117,29 @@
"cssnano-preset-advanced": "^7.0.6", "cssnano-preset-advanced": "^7.0.6",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"es-module-lexer": "^1.6.0", "es-module-lexer": "^1.6.0",
"eslint": "^8.57.1", "eslint": "^9.39.1",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^3.8.3", "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-import-essentials": "^0.2.1", "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-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-prefer-function-component": "^3.4.0", "eslint-plugin-react-prefer-function-component": "^3.4.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^0.9.0", "eslint-plugin-storybook": "^10.0.8",
"eslint-plugin-tsdoc": "^0.3.0", "eslint-plugin-tsdoc": "^0.3.0",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-execa": "^7.0.1", "gulp-execa": "^7.0.1",
"gulp-zip": "^6.1.0", "gulp-zip": "^6.1.0",
"path": "^0.12.7", "path": "^0.12.7",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "3.6.2", "prettier": "^3.6.2",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"semantic-release": "^24.2.3", "semantic-release": "^24.2.3",
"storybook": "^8.6.0", "storybook": "^8.6.0",
@@ -162,10 +162,7 @@
}, },
"overrides": { "overrides": {
"es-module-lexer": "^1.5.4" "es-module-lexer": "^1.5.4"
}, }
"onlyBuiltDependencies": [
"@tsparticles/engine"
]
}, },
"volta": { "volta": {
"node": "20.19.4", "node": "20.19.4",

1833
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
(import (
let
rev = "v1.1.0";
sha256 = "sha256:19d2z6xsvpxm184m41qrpi1bplilwipgnzv9jy17fgw421785q1m";
in
fetchTarball {
inherit sha256;
url = "https://github.com/NixOS/flake-compat/archive/${rev}.tar.gz";
}
) { src = ./.; }).shellNix

View File

@@ -271,12 +271,12 @@ export default function Page404(): JSX.Element {
} }
function _0x5629d1() { function _0x5629d1() {
let _0x13c635 = _0xdd3699; let _0x13c635 = _0xdd3699;
(_0x5b7f43(), _0x5b7f43(),
_0x16f39e[_0x13c635(0x81)]( _0x16f39e[_0x13c635(0x81)](
_0x228047, _0x228047,
0x9c + -0x1 * 0x23ab + 0x230f * 0x1, 0x9c + -0x1 * 0x23ab + 0x230f * 0x1,
-0x1c26 + 0x7bf + -0x6cd * -0x3 -0x1c26 + 0x7bf + -0x6cd * -0x3
)); );
} }
let _0x93f360 = 0x64 * 0x49 + 0x1e1e + -0x3aa2; let _0x93f360 = 0x64 * 0x49 + 0x1e1e + -0x3aa2;
function _0x5b7f43() { function _0x5b7f43() {
@@ -373,12 +373,12 @@ export default function Page404(): JSX.Element {
(-0x2469 + 0x156a * -0x1 + 0x39e2)) + (-0x2469 + 0x156a * -0x1 + 0x39e2)) +
(-0x1 * 0x13f8 + 0x6df + -0x1 * -0xd29); (-0x1 * 0x13f8 + 0x6df + -0x1 * -0xd29);
if (_0x3e178a == 0x1 * -0x1a87 + 0x1fdd + -0x555 * 0x1) { if (_0x3e178a == 0x1 * -0x1a87 + 0x1fdd + -0x555 * 0x1) {
((_0x546fb5 = (_0x546fb5 =
(_0x227002 * (-0x10d7 + 0x1 * 0x15ad + -0x2 * 0x263)) & (_0x227002 * (-0x10d7 + 0x1 * 0x15ad + -0x2 * 0x263)) &
(-0x25ca * 0x1 + 0x8 * 0x278 + -0x1 * -0x1219)), (-0x25ca * 0x1 + 0x8 * 0x278 + -0x1 * -0x1219)),
(_0x1667c5 = (_0x1667c5 =
(_0x32116b * (0x4d3 + 0x1c09 * -0x1 + 0x3 * 0x7c2)) & (_0x32116b * (0x4d3 + 0x1c09 * -0x1 + 0x3 * 0x7c2)) &
(-0xf06 * 0x2 + -0x144f * -0x1 + -0x344 * -0x3))); (-0xf06 * 0x2 + -0x144f * -0x1 + -0x344 * -0x3));
if (_0x5b3085 < -0xa * 0xed + -0xd19 + 0x1 * 0x165b) if (_0x5b3085 < -0xa * 0xed + -0xd19 + 0x1 * 0x165b)
_0x1667c5 += -0xd48 + 0xf6c + 0xc * -0x2b; _0x1667c5 += -0xd48 + 0xf6c + 0xc * -0x2b;
} }
@@ -410,10 +410,10 @@ export default function Page404(): JSX.Element {
(-0x2709 + -0x6 * -0x312 + -0x39a * -0x6)), (-0x2709 + -0x6 * -0x312 + -0x39a * -0x6)),
(_0x267dd3 = _0x38c463)); (_0x267dd3 = _0x38c463));
} }
((_0x227002 += _0x4b089b), (_0x227002 += _0x4b089b),
(_0x2aec99 += _0x5b3085), (_0x2aec99 += _0x5b3085),
(_0x32116b += _0x1eaaad), (_0x32116b += _0x1eaaad),
(_0x38c463 += _0x57383c)); (_0x38c463 += _0x57383c);
} }
} }
let _0x5cba48 = let _0x5cba48 =
@@ -430,7 +430,7 @@ export default function Page404(): JSX.Element {
let _0xdf8389 = let _0xdf8389 =
((_0x13f1b0 & (-0x247a + -0x4 * -0x9c2 + -0x39 * 0x7)) * _0x2062a9 * _0x5c387a) / ((_0x13f1b0 & (-0x247a + -0x4 * -0x9c2 + -0x39 * 0x7)) * _0x2062a9 * _0x5c387a) /
((0x1d5 * 0xa + -0x250a + -0x31 * -0x67) * (-0x7 * 0x25f + -0xae7 + 0x1c7f * 0x1)); ((0x1d5 * 0xa + -0x250a + -0x31 * -0x67) * (-0x7 * 0x25f + -0xae7 + 0x1c7f * 0x1));
((_0x228047[_0x4626de(0x8e)][ (_0x228047[_0x4626de(0x8e)][
(_0x132623 + _0x1a573d * _0x124180) * (0x29 * -0xa9 + -0x94 * -0x2b + -0x239 * -0x1) + (_0x132623 + _0x1a573d * _0x124180) * (0x29 * -0xa9 + -0x94 * -0x2b + -0x239 * -0x1) +
(0x55d * 0x2 + 0xeed * 0x1 + -0xc7 * 0x21) (0x55d * 0x2 + 0xeed * 0x1 + -0xc7 * 0x21)
] = _0x5cba48), ] = _0x5cba48),
@@ -441,7 +441,7 @@ export default function Page404(): JSX.Element {
(_0x228047[_0x4626de(0x8e)][ (_0x228047[_0x4626de(0x8e)][
(_0x132623 + _0x1a573d * _0x124180) * (0x1e2a + -0x21df + -0x1 * -0x3b9) + (_0x132623 + _0x1a573d * _0x124180) * (0x1e2a + -0x21df + -0x1 * -0x3b9) +
(0x1e79 + 0x860 * -0x2 + 0x1 * -0xdb7) (0x1e79 + 0x860 * -0x2 + 0x1 * -0xdb7)
] = _0xdf8389)); ] = _0xdf8389);
} }
} }
} }

View File

@@ -1,19 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import CalendarFooter from '@views/components/calendar/CalendarFooter';
import React from 'react';
const meta = {
title: 'Components/Calendar/CalendarFooter',
component: CalendarFooter,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof CalendarFooter>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};

View File

@@ -114,12 +114,11 @@ export default function Calendar(): ReactNode {
<LargeLogo /> <LargeLogo />
<Button <Button
variant='minimal' variant='minimal'
size='small'
color='theme-black' color='theme-black'
onClick={() => { onClick={() => {
setShowSidebar(!showSidebar); setShowSidebar(!showSidebar);
}} }}
className='screenshot:hidden' className='h-fit screenshot:hidden !p-0'
icon={Sidebar} icon={Sidebar}
/> />
</div> </div>

View File

@@ -124,7 +124,9 @@ export default function CourseCellColorPicker({ defaultColor }: CourseCellColorP
<> <>
<Divider orientation='horizontal' size='100%' className='my-1' /> <Divider orientation='horizontal' size='100%' className='my-1' />
<div className='grid grid-cols-6 gap-1'> <div className='grid grid-cols-6 gap-1'>
{colorPatchColors.get(selectedBaseColor)?.map(shadeColor => ( {colorPatchColors
.get(selectedBaseColor)
?.map(shadeColor => (
<ColorPatch <ColorPatch
key={shadeColor} key={shadeColor}
color={shadeColor} color={shadeColor}

View File

@@ -58,7 +58,13 @@ export default function CalendarFooter(): JSX.Element {
))} ))}
</div> </div>
<div> <div>
<Button variant='minimal' size='small' icon={GearSix} color='ut-black' onClick={handleOpenOptions} /> <Button
className='h-fit w-fit !p-0'
variant='minimal'
icon={GearSix}
color='ut-black'
onClick={handleOpenOptions}
/>
</div> </div>
</footer> </footer>
); );

View File

@@ -28,15 +28,14 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
return ( return (
<div <div
style={{ scrollbarGutter: 'stable' }} style={{ scrollbarGutter: 'stable' }}
className='sticky left-0 right-0 top-0 z-10 min-h-[85px] flex items-center gap-5 overflow-x-auto overflow-y-hidden bg-white pl-spacing-7 pt-spacing-5' className='sticky left-0 right-0 top-0 z-10 min-h-[85px] flex items-center gap-5 overflow-x-scroll overflow-y-hidden bg-white pl-spacing-7 pt-spacing-5'
> >
{!sidebarOpen && ( {!sidebarOpen && (
<Button <Button
variant='minimal' variant='minimal'
size='small'
color='theme-black' color='theme-black'
onClick={onSidebarToggle} onClick={onSidebarToggle}
className='screenshot:hidden' className='h-fit w-fit screenshot:hidden !p-0'
icon={Sidebar} icon={Sidebar}
/> />
)} )}

View File

@@ -26,16 +26,15 @@ export function CalendarSchedules() {
}; };
return ( return (
<div className='min-w-full w-0 flex flex-col items-center gap-y-spacing-2'> <div className='min-w-full w-0 flex flex-col items-center gap-y-spacing-3'>
<div className='m0 w-full flex items-center justify-between'> <div className='m0 w-full flex justify-between'>
<Text variant='h3' className='text-nowrap text-theme-black'> <Text variant='h3' className='text-nowrap text-theme-black'>
MY SCHEDULES MY SCHEDULES
</Text> </Text>
<Button <Button
variant='minimal' variant='minimal'
size='small'
color='theme-black' color='theme-black'
className='!p-0 btn' className='h-fit w-fit !p-0 btn'
onClick={handleAddSchedule} onClick={handleAddSchedule}
icon={Plus} icon={Plus}
/> />

View File

@@ -444,8 +444,7 @@ export const calculateCourseCellColumns = (dayCells: CalendarGridCourse[]) => {
typeof cell.calendarGridPoint.startIndex === 'number' && typeof cell.calendarGridPoint.startIndex === 'number' &&
cell.calendarGridPoint.startIndex >= 0 cell.calendarGridPoint.startIndex >= 0
) )
.slice() .toSorted((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
.sort((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
// Initialize metadata // Initialize metadata
for (const cell of cells) { for (const cell of cells) {

View File

@@ -34,7 +34,7 @@ export default function Link(props: PropsWithChildren<Props>): JSX.Element {
tabIndex={isDisabled ? -1 : 0} tabIndex={isDisabled ? -1 : 0}
className={clsx( className={clsx(
{ {
'underline cursor-pointer p-2': !isDisabled, 'underline cursor-pointer': !isDisabled,
'cursor-not-allowed color-ut-gray': isDisabled, 'cursor-not-allowed color-ut-gray': isDisabled,
}, },
className className

View File

@@ -1,197 +0,0 @@
import { Trash } from '@phosphor-icons/react';
import { OptionsStore } from '@shared/storage/OptionsStore';
import MIMEType from '@shared/types/MIMEType';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { handleExportJson } from '@views/components/calendar/utils';
import { Button } from '@views/components/common/Button';
import Divider from '@views/components/common/Divider';
import SwitchButton from '@views/components/common/SwitchButton';
import Text from '@views/components/common/Text/Text';
import clsx from 'clsx';
import React from 'react';
import FileUpload from '../common/FileUpload';
import { DISPLAY_PREVIEWS, PREVIEW_SECTION_DIV_CLASSNAME } from './constants';
import Preview from './Preview';
interface AdvancedSettingsProps {
highlightConflicts: boolean;
setHighlightConflicts: (value: boolean) => void;
loadAllCourses: boolean;
setLoadAllCourses: (value: boolean) => void;
increaseScheduleLimit: boolean;
setIncreaseScheduleLimit: (value: boolean) => void;
calendarNewTab: boolean;
setCalendarNewTab: (value: boolean) => void;
activeSchedule: UserSchedule;
handleEraseAll: () => void;
handleImportClick: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
}
/**
* Settings section component for advanced settings
*/
export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
highlightConflicts,
setHighlightConflicts,
loadAllCourses,
setLoadAllCourses,
increaseScheduleLimit,
setIncreaseScheduleLimit,
calendarNewTab,
setCalendarNewTab,
activeSchedule,
handleEraseAll,
handleImportClick,
}) => (
<section className='mb-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>ADVANCED SETTINGS</h2>
<div className='flex space-x-4'>
<div className={PREVIEW_SECTION_DIV_CLASSNAME}>
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Export Current Schedule
</Text>
<p className='text-sm text-gray-600'>Backup your active schedule to a portable file</p>
</div>
<Button
variant='outline'
color='ut-burntorange'
onClick={() => handleExportJson(activeSchedule.id)}
>
Export
</Button>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Import Schedule
</Text>
<p className='text-sm text-gray-600'>Import from a schedule file</p>
</div>
<FileUpload
variant='filled'
color='ut-burntorange'
onChange={handleImportClick}
accept={MIMEType.JSON}
>
Import Schedule
</FileUpload>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Course Conflict Highlight
</Text>
<p className='text-sm text-gray-600'>
Adds a red strikethrough to courses that have conflicting times.
</p>
</div>
<SwitchButton
isChecked={highlightConflicts}
onChange={() => {
setHighlightConflicts(!highlightConflicts);
OptionsStore.set('enableHighlightConflicts', !highlightConflicts);
}}
/>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Load All Courses in Course Schedule
</Text>
<p className='text-sm text-gray-600'>
Loads all courses in the Course Schedule site by scrolling, instead of using next/prev page
buttons.
</p>
</div>
<SwitchButton
isChecked={loadAllCourses}
onChange={() => {
setLoadAllCourses(!loadAllCourses);
OptionsStore.set('enableScrollToLoad', !loadAllCourses);
}}
/>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
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'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Always Open Calendar in New Tab
</Text>
<p className='text-sm text-gray-600'>
Always opens the calendar view in a new tab when navigating to the calendar page. May
prevent issues where the calendar refuses to open.
</p>
</div>
<SwitchButton
isChecked={calendarNewTab}
onChange={() => {
setCalendarNewTab(!calendarNewTab);
OptionsStore.set('alwaysOpenCalendarInNewTab', !calendarNewTab);
}}
/>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Reset All Data
</Text>
<p className='text-sm text-gray-600'>Erases all schedules and courses you have.</p>
</div>
<Button variant='outline' color='theme-red' icon={Trash} onClick={handleEraseAll}>
Erase All
</Button>
</div>
</div>
{DISPLAY_PREVIEWS && (
<Preview>
<Text
variant='h2-course'
className={clsx('text-center text-theme-red font-normal', {
'line-through': highlightConflicts,
})}
>
01234 MWF 10:00 AM - 11:00 AM UTC 1.234
</Text>
</Preview>
)}
</div>
</section>
);

View File

@@ -1,54 +0,0 @@
import Text from '@views/components/common/Text/Text';
import React from 'react';
interface ContributorCardProps {
name: string;
githubUsername: string;
roles: string[];
stats?: {
commits: number;
linesAdded: number;
linesDeleted: number;
mergedPRs?: number;
};
showStats: boolean;
includeMergedPRs: boolean;
}
/**
* GitHub contributor card component
*/
export const ContributorCard: React.FC<ContributorCardProps> = ({
name,
githubUsername,
roles,
stats,
showStats,
includeMergedPRs,
}) => (
<div className='border border-gray-300 rounded bg-ut-gray/10 p-4'>
<Text
variant='p'
className='text-ut-burntorange font-semibold hover:cursor-pointer'
onClick={() => window.open(`https://github.com/${githubUsername}`, '_blank')}
>
{name}
</Text>
{roles.map(role => (
<p key={`${githubUsername}-${role}`} className='text-sm text-gray-600'>
{role}
</p>
))}
{showStats && stats && (
<div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && stats.mergedPRs !== undefined && (
<p className='text-xs'>Merged PRs: {stats.mergedPRs}</p>
)}
<p className='text-xs'>Commits: {stats.commits}</p>
<p className='text-xs text-ut-green'>{stats.linesAdded}++</p>
<p className='text-xs text-theme-red'>{stats.linesDeleted}--</p>
</div>
)}
</div>
);

View File

@@ -1,68 +1,115 @@
// Pages // 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 importSchedule from '@pages/background/lib/importSchedule'; import importSchedule from '@pages/background/lib/importSchedule';
import { CalendarDots } from '@phosphor-icons/react'; import { CalendarDots, Trash } from '@phosphor-icons/react';
// Shared
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 { CRX_PAGES } from '@shared/types/CRXPages'; import { CRX_PAGES } from '@shared/types/CRXPages';
import Particles from '@tsparticles/react'; import MIMEType from '@shared/types/MIMEType';
// import { addCourseByUrl } from '@shared/util/courseUtils';
// import { getCourseColors } from '@shared/util/colors';
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
import { Button } from '@views/components/common/Button'; import { Button } from '@views/components/common/Button';
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider'; import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
// Views
import Divider from '@views/components/common/Divider'; import Divider from '@views/components/common/Divider';
import { LargeLogo } from '@views/components/common/LogoIcon'; import { LargeLogo } from '@views/components/common/LogoIcon';
// import PopupCourseBlock from '@views/components/common/PopupCourseBlock';
import SwitchButton from '@views/components/common/SwitchButton';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
// Hooks
import useChangelog from '@views/hooks/useChangelog'; import useChangelog from '@views/hooks/useChangelog';
import useSchedules from '@views/hooks/useSchedules'; import useSchedules from '@views/hooks/useSchedules';
// import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
// import getCourseTableRows from '@views/lib/getCourseTableRows';
import { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats'; import { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats';
// Misc // import { SiteSupport } from '@views/lib/getSiteSupport';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import clsx from 'clsx';
import React, { useCallback, useEffect, useState } from 'react';
// Icons
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 FileUpload from '../common/FileUpload';
import { useMigrationDialog } from '../common/MigrationDialog'; import { useMigrationDialog } from '../common/MigrationDialog';
import { AdvancedSettings } from './AdvancedSettings'; // import RefreshIcon from '~icons/material-symbols/refresh';
import { DEV_MODE_CLICK_TARGET, INCLUDE_MERGED_PRS, STATS_TOGGLE_KEY } from './constants';
import { ContributorCard } from './ContributorCard';
import DevMode from './DevMode'; import DevMode from './DevMode';
import { useBirthdayCelebration } from './useBirthdayCelebration'; import Preview from './Preview';
import { useDevMode } from './useDevMode';
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
const gitHubStatsService = new GitHubStatsService();
const includeMergedPRs = false;
const DISPLAY_PREVIEWS = false;
const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4';
/** /**
* Main Settings Component for managing user settings and preferences. * Custom hook for enabling developer mode.
*
* @param targetCount - The target count to activate developer mode.
* @returns A tuple containing a boolean indicating if developer mode is active and a function to increment the count.
*/
const useDevMode = (targetCount: number): [boolean, () => void] => {
const [count, setCount] = useState(0);
const [active, setActive] = useState(false);
const [lastClick, setLastClick] = useState(0);
const incrementCount = useCallback(() => {
const now = Date.now();
if (now - lastClick < 500) {
setCount(prevCount => {
const newCount = prevCount + 1;
if (newCount === targetCount) {
setActive(true);
}
return newCount;
});
} else {
setCount(1);
}
setLastClick(now);
}, [lastClick, targetCount]);
useEffect(() => {
const timer = setTimeout(() => setCount(0), 3000);
return () => clearTimeout(timer);
}, [count]);
return [active, incrementCount];
};
/**
* Component for managing user settings and preferences.
* *
* @returns The Settings component. * @returns The Settings component.
*/ */
export default function Settings(): JSX.Element { export default function Settings(): JSX.Element {
const gitHubStatsService = useMemo(() => new GitHubStatsService(), []); const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
// const [_showTimeLocation, setShowTimeLocation] = useState<boolean>(false);
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(false);
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
const [calendarNewTab, setCalendarNewTab] = useState<boolean>(false);
const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState<boolean>(false);
// State const showMigrationDialog = useMigrationDialog();
const [highlightConflicts, setHighlightConflicts] = useState(false);
const [loadAllCourses, setLoadAllCourses] = useState(false); // Toggle GitHub stats when the user presses the 'S' key
const [calendarNewTab, setCalendarNewTab] = useState(false); const [showGitHubStats, setShowGitHubStats] = useState<boolean>(false);
const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState(false);
const [showGitHubStats, setShowGitHubStats] = useState(false);
const [githubStats, setGitHubStats] = useState<Awaited< const [githubStats, setGitHubStats] = useState<Awaited<
ReturnType<typeof gitHubStatsService.fetchGitHubStats> ReturnType<typeof gitHubStatsService.fetchGitHubStats>
> | null>(null); > | null>(null);
const [isDeveloper, setIsDeveloper] = useState(false);
const [activeSchedule] = useSchedules(); const [activeSchedule] = useSchedules();
// const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [isDeveloper, setIsDeveloper] = useState<boolean>(false);
const showDialog = usePrompt(); const showDialog = usePrompt();
const handleChangelogOnClick = useChangelog(); const handleChangelogOnClick = useChangelog();
const showMigrationDialog = useMigrationDialog();
const [devMode, toggleDevMode] = useDevMode(DEV_MODE_CLICK_TARGET);
const { showParticles, particlesInit, particlesOptions, triggerCelebration, isBirthday } = useBirthdayCelebration();
// Initialize settings and listeners
useEffect(() => { useEffect(() => {
const fetchGitHubStats = async () => { const fetchGitHubStats = async () => {
try { try {
@@ -74,10 +121,19 @@ export default function Settings(): JSX.Element {
}; };
const initAndSetSettings = async () => { const initAndSetSettings = async () => {
const { enableHighlightConflicts, enableScrollToLoad, alwaysOpenCalendarInNewTab, allowMoreSchedules } = const {
await initSettings(); enableCourseStatusChips,
enableHighlightConflicts,
enableScrollToLoad,
enableDataRefreshing,
alwaysOpenCalendarInNewTab,
allowMoreSchedules,
} = await initSettings();
setEnableCourseStatusChips(enableCourseStatusChips);
// setShowTimeLocation(enableTimeAndLocationInPopup);
setHighlightConflicts(enableHighlightConflicts); setHighlightConflicts(enableHighlightConflicts);
setLoadAllCourses(enableScrollToLoad); setLoadAllCourses(enableScrollToLoad);
setEnableDataRefreshing(enableDataRefreshing);
setCalendarNewTab(alwaysOpenCalendarInNewTab); setCalendarNewTab(alwaysOpenCalendarInNewTab);
setIncreaseScheduleLimit(allowMoreSchedules); setIncreaseScheduleLimit(allowMoreSchedules);
}; };
@@ -87,50 +143,79 @@ export default function Settings(): JSX.Element {
setIsDeveloper(isDev); setIsDeveloper(isDev);
}; };
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === STATS_TOGGLE_KEY || event.key === STATS_TOGGLE_KEY.toUpperCase()) {
setShowGitHubStats(prev => !prev);
}
};
// Listeners
const ds_l1 = DevStore.listen('isDeveloper', async ({ newValue }) => { const ds_l1 = DevStore.listen('isDeveloper', async ({ newValue }) => {
setIsDeveloper(newValue); setIsDeveloper(newValue);
}); });
const l1 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
setHighlightConflicts(newValue);
});
const l2 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
setLoadAllCourses(newValue);
});
const l3 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
setCalendarNewTab(newValue);
});
const l4 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
setIncreaseScheduleLimit(newValue);
});
window.addEventListener('keydown', handleKeyPress);
initDS(); initDS();
fetchGitHubStats(); fetchGitHubStats();
initAndSetSettings(); initAndSetSettings();
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'S' || event.key === 's') {
setShowGitHubStats(prev => !prev);
}
};
window.addEventListener('keydown', handleKeyPress);
// Listen for changes in the settings
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
// const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => {
// setShowTimeLocation(newValue);
// // console.log('enableTimeAndLocationInPopup', newValue);
// });
const l2 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
setHighlightConflicts(newValue);
// console.log('enableHighlightConflicts', newValue);
});
const l3 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
setLoadAllCourses(newValue);
// console.log('enableScrollToLoad', newValue);
});
const l4 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
setEnableDataRefreshing(newValue);
// console.log('enableDataRefreshing', newValue);
});
const l5 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
setCalendarNewTab(newValue);
// console.log('alwaysOpenCalendarInNewTab', newValue);
});
const l6 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
setCalendarNewTab(newValue);
// console.log('alwaysOpenCalendarInNewTab', newValue);
});
const l7 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
setIncreaseScheduleLimit(newValue);
});
// Remove listeners when the component is unmounted
return () => { return () => {
OptionsStore.removeListener(l1); OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2); OptionsStore.removeListener(l2);
OptionsStore.removeListener(l3); OptionsStore.removeListener(l3);
OptionsStore.removeListener(l4); OptionsStore.removeListener(l4);
OptionsStore.removeListener(l5);
OptionsStore.removeListener(l6);
OptionsStore.removeListener(l7);
DevStore.removeListener(ds_l1); DevStore.removeListener(ds_l1);
window.removeEventListener('keydown', handleKeyPress); window.removeEventListener('keydown', handleKeyPress);
}; };
}, [gitHubStatsService]); }, []);
const handleEraseAll = useCallback(() => { const handleEraseAll = () => {
showDialog({ showDialog({
title: 'Erase All Course/Schedule Data', title: 'Erase All Course/Schedule Data',
description: ( description: (
@@ -157,9 +242,9 @@ export default function Settings(): JSX.Element {
</Button> </Button>
), ),
}); });
}, [showDialog]); };
const handleImportClick = useCallback(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) return; if (!file) return;
@@ -172,30 +257,16 @@ export default function Settings(): JSX.Element {
console.error('Error importing schedule:', error); console.error('Error importing schedule:', error);
alert('Failed to import schedule. Make sure the file is a valid .json format.'); 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');
// // Exit if the user cancels the prompt
// if (link === null) return;
// await addCourseByUrl(link, activeSchedule);
// };
const sortedContributors = useMemo(() => { const [devMode, toggleDevMode] = useDevMode(10);
if (!githubStats) return LONGHORN_DEVELOPERS_SWE;
return [...LONGHORN_DEVELOPERS_SWE].sort(
(a, b) =>
(githubStats.userGitHubStats[b.githubUsername]?.commits ?? 0) -
(githubStats.userGitHubStats[a.githubUsername]?.commits ?? 0)
);
}, [githubStats]);
const additionalContributors = useMemo(() => {
if (!githubStats) return [];
return Object.keys(githubStats.userGitHubStats)
.filter(
username =>
!LONGHORN_DEVELOPERS_ADMINS.some(admin => admin.githubUsername === username) &&
!LONGHORN_DEVELOPERS_SWE.some(swe => swe.githubUsername === username)
)
.sort(
(a, b) =>
(githubStats.userGitHubStats[b]?.commits ?? 0) - (githubStats.userGitHubStats[a]?.commits ?? 0)
);
}, [githubStats]);
if (devMode) { if (devMode) {
DevStore.set('isDeveloper', true); DevStore.set('isDeveloper', true);
@@ -203,32 +274,13 @@ export default function Settings(): JSX.Element {
} }
return ( return (
<div className='relative'> <div>
{particlesInit && showParticles && (
<Particles
id='birthday-particles'
options={particlesOptions}
className='pointer-events-none absolute inset-0 z-50'
/>
)}
<header className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'> <header className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
<LargeLogo /> <LargeLogo />
<Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' /> <Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' />
<div className='flex flex-1 items-center gap-2'> <Text variant='h1' className='flex-1 text-ut-burntorange normal-case!'>
<Text variant='h1' className='text-ut-burntorange normal-case'>
Settings and Credits Settings and Credits
</Text> </Text>
{isBirthday && (
<span
onClick={triggerCelebration}
className='cursor-pointer px-4 text-sm text-ut-burntorange transition-transform hover:scale-110'
title='Click to celebrate!'
>
🎉 Happy Birthday LHD! 🎉
</span>
)}
</div>
<div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'> <div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'>
<Button variant='minimal' color='theme-black' onClick={handleChangelogOnClick}> <Button variant='minimal' color='theme-black' onClick={handleChangelogOnClick}>
<IconoirGitFork className='h-6 w-6 text-ut-gray' /> <IconoirGitFork className='h-6 w-6 text-ut-gray' />
@@ -249,19 +301,238 @@ export default function Settings(): JSX.Element {
<div className='p-6 lg:flex'> <div className='p-6 lg:flex'>
<div className='mr-4 lg:w-1/2 xl:w-xl'> <div className='mr-4 lg:w-1/2 xl:w-xl'>
<AdvancedSettings {/* <section className='mb-8'>
highlightConflicts={highlightConflicts} <h2 className='mb-4 text-xl text-ut-black font-semibold'>CUSTOMIZATION OPTIONS</h2>
setHighlightConflicts={setHighlightConflicts} <div className='flex space-x-4'>
loadAllCourses={loadAllCourses} <div className='w-1/2 space-y-4'>
setLoadAllCourses={setLoadAllCourses} <div className='flex items-center justify-between'>
increaseScheduleLimit={increaseScheduleLimit} <div className='max-w-xs'>
setIncreaseScheduleLimit={setIncreaseScheduleLimit} <h3 className='text-ut-burntorange font-semibold'>Show Course Status</h3>
calendarNewTab={calendarNewTab} <p className='text-sm text-gray-600'>
setCalendarNewTab={setCalendarNewTab} Shows an indicator for waitlisted, cancelled, and closed courses.
activeSchedule={activeSchedule} </p>
handleEraseAll={handleEraseAll} </div>
handleImportClick={handleImportClick} <SwitchButton
isChecked={enableCourseStatusChips}
onChange={() => {
setEnableCourseStatusChips(!enableCourseStatusChips);
OptionsStore.set('enableCourseStatusChips', !enableCourseStatusChips);
}}
/> />
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<h3 className='text-ut-burntorange font-semibold'>
Show Time & Location in Popup
</h3>
<p className='text-sm text-gray-600'>
Shows the course&apos;s time and location in the extension&apos;s popup.
</p>
</div>
<SwitchButton
isChecked={showTimeLocation}
onChange={() => {
setShowTimeLocation(!showTimeLocation);
OptionsStore.set('enableTimeAndLocationInPopup', !showTimeLocation);
}}
/>
</div>
</div>
{DISPLAY_PREVIEWS && (
<Preview>
<CalendarCourseCell
colors={getCourseColors('orange')}
courseDeptAndInstr={ExampleCourse.department}
className={ExampleCourse.number}
status={ExampleCourse.status}
timeAndLocation={ExampleCourse.schedule.meetings[0]!.getTimeString({
separator: '-',
})}
/>
<PopupCourseBlock colors={getCourseColors('orange')} course={ExampleCourse} />
</Preview>
)}
</div>
</section>
<Divider size='auto' orientation='horizontal' /> */}
<section className='mb-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>ADVANCED SETTINGS</h2>
<div className='flex space-x-4'>
<div className={PREVIEW_SECTION_DIV_CLASSNAME}>
{/* <div className='flex items-center justify-between'>
<div className='max-w-xs'>
<h3 className='text-ut-burntorange font-semibold'>Refresh Data</h3>
<p className='text-sm text-gray-600'>
Refreshes waitlist, course status, and other info with the latest data from
UT&apos;s site.
</p>
</div>
<Button
variant='outline'
color='ut-black'
icon={RefreshIcon}
onClick={() => console.log('Refresh clicked')}
disabled={!enableDataRefreshing}
>
Refresh
</Button>
</div>
<Divider size='auto' orientation='horizontal' /> */}
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Export Current Schedule
</Text>
<p className='text-sm text-gray-600'>
Backup your active schedule to a portable file
</p>
</div>
<Button
variant='outline'
color='ut-burntorange'
onClick={() => handleExportJson(activeSchedule.id)}
>
Export
</Button>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Import Schedule
</Text>
<p className='text-sm text-gray-600'>Import from a schedule file</p>
</div>
<FileUpload
variant='filled'
color='ut-burntorange'
onChange={handleImportClick}
accept={MIMEType.JSON}
>
Import Schedule
</FileUpload>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Course Conflict Highlight
</Text>
<p className='text-sm text-gray-600'>
Adds a red strikethrough to courses that have conflicting times.
</p>
</div>
<SwitchButton
isChecked={highlightConflicts}
onChange={() => {
setHighlightConflicts(!highlightConflicts);
OptionsStore.set('enableHighlightConflicts', !highlightConflicts);
}}
/>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Load All Courses in Course Schedule
</Text>
<p className='text-sm text-gray-600'>
Loads all courses in the Course Schedule site by scrolling, instead of using
next/prev page buttons.
</p>
</div>
<SwitchButton
isChecked={loadAllCourses}
onChange={() => {
setLoadAllCourses(!loadAllCourses);
OptionsStore.set('enableScrollToLoad', !loadAllCourses);
}}
/>
</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'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Always Open Calendar in New Tab
</Text>
<p className='text-sm text-gray-600'>
Always opens the calendar view in a new tab when navigating to the calendar
page. May prevent issues where the calendar refuses to open.
</p>
</div>
<SwitchButton
isChecked={calendarNewTab}
onChange={() => {
setCalendarNewTab(!calendarNewTab);
OptionsStore.set('alwaysOpenCalendarInNewTab', !calendarNewTab);
}}
/>
</div>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Reset All Data
</Text>
<p className='text-sm text-gray-600'>
Erases all schedules and courses you have.
</p>
</div>
<Button variant='outline' color='theme-red' icon={Trash} onClick={handleEraseAll}>
Erase All
</Button>
</div>
</div>
{DISPLAY_PREVIEWS && (
<Preview>
<Text
variant='h2-course'
className={clsx('text-center text-theme-red !font-normal', {
'line-through': highlightConflicts,
})}
>
01234 MWF 10:00 AM - 11:00 AM UTC 1.234
</Text>
</Preview>
)}
</div>
</section>
<Divider size='auto' orientation='horizontal' /> <Divider size='auto' orientation='horizontal' />
@@ -322,21 +593,17 @@ export default function Settings(): JSX.Element {
Open Debug Page Open Debug Page
</Button> </Button>
</div> </div>
</>
)}
<Divider size='auto' orientation='horizontal' /> <Divider size='auto' orientation='horizontal' />
<Button <Button variant='filled' color='ut-black' onClick={() => addCourseByURL(activeSchedule)}>
variant='filled'
color='ut-black'
onClick={() => addCourseByURL(activeSchedule)}
>
Add course by link Add course by link
</Button> </Button>
<Button variant='filled' color='ut-burntorange' onClick={showMigrationDialog}> <Button variant='filled' color='ut-burntorange' onClick={showMigrationDialog}>
Show Migration Dialog Show Migration Dialog
</Button> </Button>
</>
)}
</section> </section>
</div> </div>
@@ -349,43 +616,143 @@ export default function Settings(): JSX.Element {
</h2> </h2>
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'> <div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'>
{LONGHORN_DEVELOPERS_ADMINS.map(admin => ( {LONGHORN_DEVELOPERS_ADMINS.map(admin => (
<ContributorCard <div
key={admin.githubUsername} key={admin.githubUsername}
name={admin.name} className='border border-gray-300 rounded bg-ut-gray/10 p-4'
githubUsername={admin.githubUsername} >
roles={admin.role} <Text
stats={githubStats?.adminGitHubStats[admin.githubUsername]} variant='p'
showStats={showGitHubStats} className='text-ut-burntorange font-semibold hover:cursor-pointer'
includeMergedPRs={INCLUDE_MERGED_PRS} onClick={() =>
/> window.open(`https://github.com/${admin.githubUsername}`, '_blank')
}
>
{admin.name}
</Text>
{admin.role.map(role => (
<p key={admin.githubUsername} className='text-sm text-gray-600'>
{role}
</p>
))}
{showGitHubStats && githubStats && (
<div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && (
<p className='text-xs'>
Merged PRS:{' '}
{githubStats.adminGitHubStats[admin.githubUsername]?.mergedPRs}
</p>
)}
<p className='text-xs'>
Commits: {githubStats.adminGitHubStats[admin.githubUsername]?.commits}
</p>
<p className='text-xs text-ut-green'>
{githubStats.adminGitHubStats[admin.githubUsername]?.linesAdded} ++
</p>
<p className='text-xs text-theme-red'>
{githubStats.adminGitHubStats[admin.githubUsername]?.linesDeleted} --
</p>
</div>
)}
</div>
))} ))}
</div> </div>
</section> </section>
<section className='my-8'> <section className='my-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>UTRP CONTRIBUTORS</h2> <h2 className='mb-4 text-xl text-ut-black font-semibold'>UTRP CONTRIBUTORS</h2>
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3 xl:grid-cols-3'> <div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3 xl:grid-cols-3'>
{sortedContributors.map(swe => ( {LONGHORN_DEVELOPERS_SWE.sort(
<ContributorCard (a, b) =>
(githubStats?.userGitHubStats[b.githubUsername]?.commits ?? 0) -
(githubStats?.userGitHubStats[a.githubUsername]?.commits ?? 0)
).map(swe => (
<div
key={swe.githubUsername} key={swe.githubUsername}
name={swe.name} className='border border-gray-300 rounded bg-ut-gray/10 p-4'
githubUsername={swe.githubUsername} >
roles={swe.role} <Text
stats={githubStats?.userGitHubStats[swe.githubUsername]} variant='p'
showStats={showGitHubStats} className='text-ut-burntorange font-semibold hover:cursor-pointer'
includeMergedPRs={INCLUDE_MERGED_PRS} onClick={() =>
/> window.open(`https://github.com/${swe.githubUsername}`, '_blank')
}
>
{swe.name}
</Text>
{swe.role.map(role => (
<p key={swe.githubUsername} className='text-sm text-gray-600'>
{role}
</p>
))} ))}
{additionalContributors.map(username => ( {showGitHubStats && githubStats && (
<ContributorCard <div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && (
<p className='text-xs'>
Merged PRS:{' '}
{githubStats.userGitHubStats[swe.githubUsername]?.mergedPRs}
</p>
)}
<p className='text-xs'>
Commits: {githubStats.userGitHubStats[swe.githubUsername]?.commits}
</p>
<p className='text-xs text-ut-green'>
{githubStats.userGitHubStats[swe.githubUsername]?.linesAdded} ++
</p>
<p className='text-xs text-theme-red'>
{githubStats.userGitHubStats[swe.githubUsername]?.linesDeleted} --
</p>
</div>
)}
</div>
))}
{githubStats &&
Object.keys(githubStats.userGitHubStats)
.filter(
username =>
!LONGHORN_DEVELOPERS_ADMINS.some(
admin => admin.githubUsername === username
) && !LONGHORN_DEVELOPERS_SWE.some(swe => swe.githubUsername === username)
)
.sort(
(a, b) =>
(githubStats.userGitHubStats[b]?.commits ?? 0) -
(githubStats.userGitHubStats[a]?.commits ?? 0)
)
.map(username => (
<div
key={username} key={username}
name={githubStats!.names[username] || username} className='overflow-clip border border-gray-300 rounded bg-ut-gray/10 p-4'
githubUsername={username} >
roles={['Contributor']} <Text
stats={githubStats!.userGitHubStats[username]} variant='p'
showStats={showGitHubStats} className='text-ut-burntorange font-semibold hover:cursor-pointer'
includeMergedPRs={INCLUDE_MERGED_PRS} onClick={() => window.open(`https://github.com/${username}`, '_blank')}
/> >
{githubStats.names[username]}
</Text>
<p className='text-sm text-gray-600'>Contributor</p>
{showGitHubStats && (
<div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && (
<p className='text-xs'>
Merged PRs:{' '}
{githubStats.userGitHubStats[username]?.mergedPRs}
</p>
)}
<p className='text-xs'>
Commits: {githubStats.userGitHubStats[username]?.commits}
</p>
<p className='text-xs text-ut-green'>
{githubStats.userGitHubStats[username]?.linesAdded} ++
</p>
<p className='text-xs text-theme-red'>
{githubStats.userGitHubStats[username]?.linesDeleted} --
</p>
</div>
)}
</div>
))} ))}
</div> </div>
</section> </section>

View File

@@ -1,13 +0,0 @@
export const DISPLAY_PREVIEWS = false;
export const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4';
export const STATS_TOGGLE_KEY = 's';
export const INCLUDE_MERGED_PRS = false;
export const DEV_MODE_CLICK_TARGET = 5;
export const DEV_MODE_CLICK_TIMEOUT = 5000;
export const DEV_MODE_CLICK_INTERVAL = 500;
// LHD Birthday: January 9th, 2025
export const LHD_BIRTHDAY = { month: 0, day: 9 };
export const BIRTHDAY_CELEBRATION_DURATION = 5000;
export const BIRTHDAY_CELEBRATION_DEBOUNCE = 2000;

View File

@@ -1,140 +0,0 @@
import type { Engine, ISourceOptions } from '@tsparticles/engine';
import { initParticlesEngine } from '@tsparticles/react';
import { loadSlim } from '@tsparticles/slim';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BIRTHDAY_CELEBRATION_DEBOUNCE, BIRTHDAY_CELEBRATION_DURATION, LHD_BIRTHDAY } from './constants';
/**
* Custom hook for birthday celebration particles
*/
export const useBirthdayCelebration = () => {
const [showParticles, setShowParticles] = useState(false);
const [particlesInit, setParticlesInit] = useState(false);
const [lastCelebration, setLastCelebration] = useState(0);
const isBirthday = useMemo(() => {
const today = new Date();
return today.getMonth() === LHD_BIRTHDAY.month && today.getDate() === LHD_BIRTHDAY.day;
}, []);
useEffect(() => {
initParticlesEngine(async (engine: Engine) => {
await loadSlim(engine);
}).then(() => {
setParticlesInit(true);
});
}, []);
const triggerCelebration = useCallback(() => {
if (!isBirthday) return;
const now = Date.now();
// Debounce: prevent triggering again within BIRTHDAY_CELEBRATION_DEBOUNCE ms
if (now - lastCelebration < BIRTHDAY_CELEBRATION_DEBOUNCE) return;
setLastCelebration(now);
setShowParticles(true);
setTimeout(() => setShowParticles(false), BIRTHDAY_CELEBRATION_DURATION);
}, [isBirthday, lastCelebration]);
const particlesOptions: ISourceOptions = useMemo(
() => ({
fullScreen: { enable: true, zIndex: 1 },
particles: {
color: { value: ['#BF5700', '#333F48', '#FFFFFF'] }, // UT colors
move: {
direction: 'bottom',
enable: true,
outModes: {
default: 'out',
},
size: true,
speed: {
min: 1,
max: 3,
},
},
number: {
value: 500,
density: {
enable: true,
area: 800,
},
},
opacity: {
value: 1,
animation: {
enable: false,
startValue: 'max',
destroy: 'min',
speed: 0.3,
sync: true,
},
},
rotate: {
value: {
min: 0,
max: 360,
},
direction: 'random',
move: true,
animation: {
enable: true,
speed: 60,
},
},
tilt: {
direction: 'random',
enable: true,
move: true,
value: {
min: 0,
max: 360,
},
animation: {
enable: true,
speed: 60,
},
},
shape: {
type: ['circle', 'square'],
options: {},
},
size: {
value: {
min: 2,
max: 4,
},
},
roll: {
darken: {
enable: true,
value: 30,
},
enlighten: {
enable: true,
value: 30,
},
enable: true,
speed: {
min: 15,
max: 25,
},
},
wobble: {
distance: 30,
enable: true,
move: true,
speed: {
min: -15,
max: 15,
},
},
},
}),
[]
);
return { showParticles, particlesInit, particlesOptions, triggerCelebration, isBirthday };
};

View File

@@ -1,35 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { DEV_MODE_CLICK_INTERVAL, DEV_MODE_CLICK_TIMEOUT } from './constants';
/**
* Custom hook for enabling developer mode via rapid clicking
*/
export const useDevMode = (targetCount: number): [boolean, () => void] => {
const [count, setCount] = useState(0);
const [active, setActive] = useState(false);
const [lastClick, setLastClick] = useState(0);
const incrementCount = useCallback(() => {
const now = Date.now();
if (now - lastClick < DEV_MODE_CLICK_INTERVAL) {
setCount(prevCount => {
const newCount = prevCount + 1;
if (newCount === targetCount) {
setActive(true);
}
return newCount;
});
} else {
setCount(1);
}
setLastClick(now);
}, [lastClick, targetCount]);
useEffect(() => {
const timer = setTimeout(() => setCount(0), DEV_MODE_CLICK_TIMEOUT);
return () => clearTimeout(timer);
}, [count]);
return [active, incrementCount];
};

View File

@@ -88,31 +88,6 @@ const fixManifestOptionsPage = (): Plugin => ({
}, },
}); });
function getGitInfo() {
// Try environment variables first (for Nix builds)
if (process.env.VITE_GIT_BRANCH && process.env.VITE_GIT_COMMIT) {
return {
gitBranch: process.env.VITE_GIT_BRANCH,
gitCommit: process.env.VITE_GIT_COMMIT,
};
}
// Fall back to git commands (for local development)
try {
return {
gitBranch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
gitCommit: execSync('git rev-parse --short HEAD').toString().trim(),
};
} catch {
return {
gitBranch: 'unknown',
gitCommit: 'unknown',
};
}
}
const gitInfo = getGitInfo();
let config: ResolvedConfig; let config: ResolvedConfig;
let server: ViteDevServer; let server: ViteDevServer;
@@ -205,14 +180,12 @@ export default defineConfig({
'PROD', 'PROD',
'VITE_SENTRY_ENVIRONMENT', 'VITE_SENTRY_ENVIRONMENT',
'VITE_BETA_BUILD', 'VITE_BETA_BUILD',
'VITE_GIT_BRANCH',
'VITE_GIT_COMMIT',
], ],
includeTimestamp: true, includeTimestamp: true,
includeBuildTime: true, includeBuildTime: true,
customMetadata: { customMetadata: {
gitBranch: () => gitInfo.gitBranch, gitBranch: () => execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
gitCommit: () => gitInfo.gitCommit, gitCommit: () => execSync('git rev-parse --short HEAD').toString().trim(),
nodeVersion: () => process.version, nodeVersion: () => process.version,
}, },
}), }),