Compare commits
307 Commits
injected-i
...
DereC4-der
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f2a29f268 | ||
|
|
d5c3d32170 | ||
|
|
149fda3b72 | ||
| 0ab83efd47 | |||
|
|
9eaff24cbb | ||
|
|
75ad4167b6 | ||
| f34dd95d77 | |||
| 9356357545 | |||
| d424ccce49 | |||
| be87e41814 | |||
| 438c82bfb5 | |||
| 0d63c66356 | |||
| 703a0fa596 | |||
| 7acc91f4ee | |||
| 5d9980cac3 | |||
| 57832b0dd4 | |||
| b9358803dd | |||
| 4397dfb1c5 | |||
| f83e012d62 | |||
| 0e70bbd52f | |||
|
|
bc5d68ce18 | ||
|
|
863d980b2d | ||
|
|
f943e4801b | ||
| 79f2ffa4cc | |||
|
|
78d749a8a9 | ||
| bc354f3798 | |||
| 8959e0d9f7 | |||
|
|
40ef92b230 | ||
|
|
4f0ae43411 | ||
| c330e3123a | |||
|
|
229a8a29e8 | ||
|
|
2f9e9b1297 | ||
|
|
6812d685d0 | ||
|
|
a2303ee35f | ||
|
|
e3c2da36df | ||
|
|
0c7cd3e790 | ||
|
|
7a40008c1e | ||
|
|
07cadb8c62 | ||
|
|
d4611952d4 | ||
|
|
88c0061187 | ||
|
|
3684ee5e9b | ||
|
|
d1b921a5b0 | ||
|
|
bcb5a8c469 | ||
| 38d09e4aae | |||
| 0534f607a5 | |||
| 98a7cc618b | |||
| bae1da43d3 | |||
|
|
a3f5e0f27f | ||
| 74cc6e6da1 | |||
| c68b017462 | |||
|
|
227de53e84 | ||
|
|
d3f64ec79e | ||
| aeb19cff54 | |||
| 5a73c8ee6c | |||
| 35b0d01c51 | |||
| b84cf7c15d | |||
|
|
8e79d6a6a8 | ||
|
|
60d1f48bd9 | ||
|
|
ee2b7c40b9 | ||
| 9b4d61c2b0 | |||
|
|
5b1e4513e2 | ||
|
|
0c42979423 | ||
|
|
36ac8607a9 | ||
|
|
4f4f34e281 | ||
|
|
8a5e12ec63 | ||
|
|
7d4c5d7be8 | ||
|
|
2dfb10e57b | ||
|
|
036cd628d3 | ||
|
|
9ee567530f | ||
|
|
e080e93faa | ||
|
|
efed1c0edb | ||
|
|
0c76052478 | ||
|
|
2af351efa8 | ||
|
|
949bbb0835 | ||
|
|
5ed81e4be9 | ||
| c5fc6219e1 | |||
| ba2bc60add | |||
|
|
8027c3d1bf | ||
|
|
dc77cc27da | ||
|
|
afa634f085 | ||
|
|
791a42bcd4 | ||
|
|
df1849180d | ||
| 27094846f7 | |||
|
|
ed4fbe5651 | ||
|
|
d04818ccd8 | ||
|
|
61c43962fb | ||
|
|
b4ad6870bf | ||
|
|
ea297d09bb | ||
|
|
85769e9d2c | ||
|
|
5714ed16d7 | ||
|
|
e919e96c53 | ||
|
|
442be8cbee | ||
|
|
df7a7c65d6 | ||
|
|
a409090b90 | ||
|
|
5cce1c79fc | ||
|
|
61c1e88dcf | ||
|
|
038ebaa268 | ||
|
|
91f62e1943 | ||
|
|
7986549fdd | ||
|
|
1d8da6579e | ||
|
|
0dff12232c | ||
|
|
a5e9e3c214 | ||
|
|
8e181b3010 | ||
|
|
44af9e16e4 | ||
|
|
7760e3acf4 | ||
|
|
a8ea3bc683 | ||
|
|
4c61ebd3fc | ||
|
|
591687eee8 | ||
|
|
261d2f2e84 | ||
|
|
3839bff29e | ||
|
|
78a6939929 | ||
|
|
3406e9a0e2 | ||
|
|
c51e6881d1 | ||
|
|
d70011016a | ||
|
|
f67280127a | ||
|
|
f932168f66 | ||
|
|
dc100b5d3a | ||
|
|
10eb9e4456 | ||
|
|
2f537b4f3e | ||
|
|
11303daebc | ||
|
|
60ab140c55 | ||
|
|
51bbd6590b | ||
|
|
346b9ced97 | ||
|
|
2992e784b0 | ||
|
|
2593b371d5 | ||
|
|
8f360206fb | ||
|
|
0c44849e15 | ||
|
|
f93a98e46a | ||
|
|
e5443122b4 | ||
|
|
28f192472b | ||
|
|
745f9dd6fb | ||
|
|
471e55dcea | ||
|
|
adbe8ac163 | ||
|
|
e44223084a | ||
|
|
7718d76be9 | ||
|
|
f22a3cd7c0 | ||
|
|
5abb2a4d4f | ||
|
|
74e6379d93 | ||
|
|
7f2a5893d4 | ||
|
|
89d03f4244 | ||
|
|
0c5bec8002 | ||
|
|
265652c420 | ||
|
|
208103d708 | ||
|
|
6ba8b68654 | ||
|
|
eba5d9f508 | ||
|
|
d9ee23c5bb | ||
|
|
d62b8d1af1 | ||
|
|
62f0851406 | ||
|
|
7ab5b157b1 | ||
|
|
a99a55788a | ||
|
|
3a48859ddd | ||
|
|
ced29975b2 | ||
|
|
84e8320e8f | ||
|
|
5f1c0231e4 | ||
|
|
b17c3fae6d | ||
|
|
19fe070491 | ||
|
|
8c069b7ad3 | ||
|
|
87799d8f02 | ||
| 07ec5abc3e | |||
|
|
ee37897df4 | ||
|
|
8a6e9070e0 | ||
|
|
8ab60c9f01 | ||
|
|
5eb7be246c | ||
|
|
fe599dfe75 | ||
|
|
23632d3c09 | ||
|
|
2a0150600f | ||
| 4d387e8063 | |||
| 3b588c2039 | |||
|
|
e73c9fe417 | ||
|
|
35fab34445 | ||
|
|
35f3c72250 | ||
|
|
c4a738f281 | ||
|
|
e0212d5109 | ||
|
|
152bc45776 | ||
|
|
91d930ee92 | ||
|
|
c6a48dd3f6 | ||
|
|
39947b3694 | ||
|
|
8df9ea55a9 | ||
|
|
bec2649fc1 | ||
|
|
1599e48d75 | ||
|
|
bda02826b1 | ||
| 58c2b4634a | |||
|
|
a30fecf8ec | ||
|
|
b27b21bbfc | ||
| 12d09b54cb | |||
|
|
25b35846e9 | ||
|
|
0f43796cd8 | ||
|
|
0f730d6c50 | ||
|
|
92462cf0df | ||
|
|
c49b094068 | ||
|
|
b34aacb067 | ||
|
|
103d2e0323 | ||
|
|
ad83ba4cdc | ||
|
|
e95ac42d57 | ||
|
|
ad18fbd162 | ||
|
|
bc3054aa43 | ||
|
|
53e7c7feda | ||
|
|
f8691207e3 | ||
|
|
5ad72af566 | ||
|
|
d1a336e903 | ||
|
|
5be79730b3 | ||
|
|
9d0f210548 | ||
|
|
387b508e40 | ||
|
|
0ed8d9c82d | ||
|
|
23276e5c7c | ||
|
|
37b5101e44 | ||
|
|
f04ff2fe26 | ||
|
|
13c69ef862 | ||
|
|
27d945f57c | ||
|
|
aef8c3d987 | ||
|
|
115d794ef9 | ||
|
|
bc935cc80a | ||
|
|
e54f488b17 | ||
|
|
cc7138949c | ||
|
|
74be880f9d | ||
|
|
5dbee6f0c3 | ||
|
|
cbb190bf4b | ||
|
|
4375118cd9 | ||
|
|
9aa78a02a5 | ||
|
|
1269731b55 | ||
|
|
79d7832d09 | ||
|
|
64fa12b10c | ||
|
|
56306ab944 | ||
|
|
923c673988 | ||
|
|
7e2f5eaed7 | ||
| 4dc8957c45 | |||
| cbb89c0db5 | |||
| ecdaadb83e | |||
| b480fa28b2 | |||
| 8493b482c4 | |||
| 18406b0c94 | |||
| 8b9cb065c2 | |||
| 7ee732b31e | |||
| e49fc295ba | |||
| b691bf3231 | |||
| 0777b822b3 | |||
| fc5af56bb7 | |||
|
|
c9d46b60ec | ||
| fd91c3b12e | |||
| 1bb6191244 | |||
| 063349d96d | |||
| d377afbf8b | |||
| ded3d96aae | |||
|
|
56f6456ce8 | ||
| 95e0544b73 | |||
| 9d6821127e | |||
|
|
0ba61534cb | ||
|
|
6cdcf4930d | ||
| b535a6eb32 | |||
|
|
0036dc5c80 | ||
|
|
dcc8a6d249 | ||
| 203b3bb340 | |||
| 200f67a1eb | |||
|
|
9105bcba15 | ||
|
|
07f75914bf | ||
|
|
cb3cb5d5fc | ||
|
|
4165d484bf | ||
|
|
7f138dafd0 | ||
|
|
5a8f6a8f1f | ||
|
|
e7ce014c70 | ||
|
|
40ece9f425 | ||
|
|
bd73e27db4 | ||
|
|
0695c70dbf | ||
|
|
9e0f9df9de | ||
|
|
8e3aa7ef33 | ||
|
|
1fdbe6294e | ||
|
|
93b65ac2ed | ||
|
|
28d93b3c25 | ||
|
|
863521fb3b | ||
|
|
837fddf804 | ||
|
|
677aa624d7 | ||
|
|
d390af3c79 | ||
|
|
6af805ba3a | ||
|
|
cd34601379 | ||
|
|
b800c58502 | ||
|
|
34a6449529 | ||
|
|
0273a23913 | ||
|
|
e6b4049403 | ||
|
|
b602b0b895 | ||
|
|
4ca97abd06 | ||
|
|
da9e7aac41 | ||
|
|
3568b8eb5e | ||
|
|
fa05d9c492 | ||
|
|
7cf34f2956 | ||
|
|
23e881f14c | ||
|
|
7eb3113ada | ||
|
|
f3a8a7db56 | ||
|
|
12f680d7e9 | ||
|
|
21b643000d | ||
|
|
7dec3c0c2a | ||
|
|
93f3a307b4 | ||
|
|
babc925967 | ||
|
|
58d7df499c | ||
|
|
b0512e392b | ||
|
|
b7878d81c6 | ||
|
|
9accd17bd4 | ||
|
|
bfeb2398aa | ||
|
|
2321540e97 | ||
|
|
ccea0f4bd1 | ||
|
|
00e00197ef | ||
|
|
85c7f7817c | ||
|
|
bb3b313fd2 | ||
|
|
52e34cb830 | ||
|
|
8b8433deaf | ||
|
|
4faca8c43b | ||
|
|
4455b10cc7 | ||
|
|
2d67b1218f |
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST_KEY=
|
||||
205
.eslintrc
@@ -1,205 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"webextensions": true,
|
||||
},
|
||||
"ignorePatterns": ["*.html", "tsconfig.json"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"airbnb-base",
|
||||
"airbnb/rules/react",
|
||||
"airbnb-typescript",
|
||||
"@unocss",
|
||||
"prettier",
|
||||
],
|
||||
"plugins": ["import", "jsdoc", "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",
|
||||
"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": [
|
||||
"warn",
|
||||
{
|
||||
"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",
|
||||
],
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@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",
|
||||
},
|
||||
}
|
||||
217
.eslintrc.cjs
Normal file
@@ -0,0 +1,217 @@
|
||||
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',
|
||||
'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',
|
||||
'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': [
|
||||
'warn',
|
||||
{
|
||||
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',
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@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',
|
||||
},
|
||||
};
|
||||
4
.github/workflows/best-practices.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
24
.github/workflows/check-types.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Type Check
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run check-types
|
||||
4
.github/workflows/chromatic.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -21,6 +21,6 @@ jobs:
|
||||
- name: Publish to Chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
projectToken: chpt_e8bd07b0b27d8eb
|
||||
exitZeroOnChanges: true
|
||||
autoAcceptChanges: 'main'
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
3
.gitignore
vendored
@@ -208,4 +208,5 @@ sketch
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
|
||||
|
||||
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||
|
||||
package-lock.json
|
||||
storybook-static/
|
||||
|
||||
@@ -34,4 +34,4 @@
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-designs'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-designs',
|
||||
'@storybook/test',
|
||||
'@chromatic-com/storybook',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {
|
||||
|
||||
@@ -1,24 +1,178 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
import type { Preview } from '@storybook/react';
|
||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||
import React from 'react';
|
||||
import ExtensionRoot from 'src/views/components/common/ExtensionRoot/ExtensionRoot';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
Story => (
|
||||
<ExtensionRoot>
|
||||
<Story />
|
||||
</ExtensionRoot>
|
||||
),
|
||||
],
|
||||
decorators: [
|
||||
Story => (
|
||||
<React.StrictMode>
|
||||
<ExtensionRoot>
|
||||
<Story />
|
||||
</ExtensionRoot>
|
||||
</React.StrictMode>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
let localData = {};
|
||||
type ListenerFunction = (
|
||||
changes: { [key: string]: chrome.storage.StorageChange },
|
||||
areaName: chrome.storage.AreaName
|
||||
) => void;
|
||||
const localDataListeners = new Map<
|
||||
ListenerFunction, // key to remove listener
|
||||
(changes: { [key: string]: chrome.storage.StorageChange }) => void
|
||||
>();
|
||||
|
||||
// mock chrome api
|
||||
globalThis.chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
/**
|
||||
* Removes all items from storage.
|
||||
* @param callback Optional.
|
||||
* Callback on success, or on failure (in which case runtime.lastError will be set).
|
||||
*/
|
||||
async clear() {
|
||||
localData = {};
|
||||
},
|
||||
/**
|
||||
* Gets one or more items from storage.
|
||||
* @param keys A single key to get, list of keys to get, or a dictionary specifying default values.
|
||||
* An empty list or object will return an empty result object. Pass in null to get the entire contents of storage.
|
||||
* @return A Promise that resolves with an object containing items
|
||||
*/
|
||||
async get(keys?: string | string[] | { [key: string]: any } | null) {
|
||||
if (keys === null) {
|
||||
return localData;
|
||||
}
|
||||
if (Array.isArray(keys)) {
|
||||
return keys.reduce((acc, key) => {
|
||||
acc[key] = localData[key];
|
||||
return acc;
|
||||
}, {} as string); // funny types
|
||||
}
|
||||
if (typeof keys === 'string') {
|
||||
return { [keys]: localData[keys] };
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
/**
|
||||
* Gets the amount of space (in bytes) being used by one or more items.
|
||||
* @param keys Optional. A single key or list of keys to get the total usage for. An empty list will return 0. Pass in null to get the total usage of all of storage.
|
||||
* @param callback Callback with the amount of space being used by storage, or on failure (in which case runtime.lastError will be set).
|
||||
* Parameter bytesInUse: Amount of space being used in storage, in bytes.
|
||||
*/
|
||||
async getBytesInUse() {
|
||||
return 0;
|
||||
},
|
||||
/**
|
||||
* Removes one or more items from storage.
|
||||
* @param keys A single key or a list of keys for items to remove.
|
||||
* @param callback Optional.
|
||||
* Callback on success, or on failure (in which case runtime.lastError will be set).
|
||||
*/
|
||||
async remove(keys: string | string[]) {
|
||||
if (Array.isArray(keys)) {
|
||||
keys.forEach(key => {
|
||||
for (const listener of localDataListeners.values()) {
|
||||
listener({ [key]: { oldValue: localData[key], newValue: undefined } });
|
||||
}
|
||||
|
||||
delete localData[key];
|
||||
});
|
||||
} else {
|
||||
for (const listener of localDataListeners.values()) {
|
||||
listener({ [keys]: { oldValue: localData[keys], newValue: undefined } });
|
||||
}
|
||||
|
||||
delete localData[keys];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Sets multiple items.
|
||||
* @param items An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected.
|
||||
* Primitive values such as numbers will serialize as expected. Values with a typeof "object" and "function" will typically serialize to {}, with the exception of Array (serializes as expected), Date, and Regex (serialize using their String representation).
|
||||
* @param callback Optional.
|
||||
* Callback on success, or on failure (in which case runtime.lastError will be set).
|
||||
*/
|
||||
async set(items: { [key: string]: any }) {
|
||||
for (const key in items) {
|
||||
const oldValue = localData[key];
|
||||
localData[key] = JSON.parse(JSON.stringify(items[key]));
|
||||
|
||||
for (const listener of localDataListeners.values()) {
|
||||
listener({ [key]: { oldValue: oldValue, newValue: localData[key] } });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
onChanged: {
|
||||
/**
|
||||
* Registers an event listener callback to an event.
|
||||
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
|
||||
*/
|
||||
addListener(
|
||||
listener: (
|
||||
changes: { [key: string]: chrome.storage.StorageChange },
|
||||
areaName: chrome.storage.AreaName
|
||||
) => void
|
||||
) {
|
||||
localDataListeners.set(listener, (changes: { [key: string]: chrome.storage.StorageChange }) => {
|
||||
listener(changes, 'local');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Deregisters an event listener callback from an event.
|
||||
* @param callback Listener that shall be unregistered.
|
||||
*/
|
||||
removeListener(listener: ListenerFunction) {
|
||||
localDataListeners.delete(listener);
|
||||
},
|
||||
},
|
||||
},
|
||||
runtime: {
|
||||
id: 'fake-id',
|
||||
getManifest(): chrome.runtime.Manifest {
|
||||
return {
|
||||
manifest_version: 3,
|
||||
name: 'fake-name',
|
||||
version: '0.0.0',
|
||||
};
|
||||
},
|
||||
onMessage: {
|
||||
/**
|
||||
* Registers an event listener callback to an event.
|
||||
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
|
||||
*/
|
||||
addListener<T extends Function>(callback: T) {},
|
||||
|
||||
/**
|
||||
* Deregisters an event listener callback from an event.
|
||||
* @param callback Listener that shall be unregistered.
|
||||
*/
|
||||
removeListener<T extends Function>(callback: T) {},
|
||||
},
|
||||
},
|
||||
} as typeof chrome;
|
||||
|
||||
// set updatedAt dates to be fixed
|
||||
|
||||
UserScheduleStore.get('schedules').then(schedules => {
|
||||
schedules.forEach(schedule => {
|
||||
schedule.updatedAt = new Date('2024-01-01 12:00').getTime();
|
||||
});
|
||||
UserScheduleStore.set('schedules', schedules);
|
||||
});
|
||||
|
||||
export default preview;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// .storybook/vite-storybook.config.ts
|
||||
import react from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/@vitejs+plugin-react-swc@3.6.0_vite@5.1.2/node_modules/@vitejs/plugin-react-swc/index.mjs";
|
||||
import { resolve } from "path";
|
||||
import UnoCSS from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/unocss@0.58.5_postcss@8.4.35_vite@5.1.2/node_modules/unocss/dist/vite.mjs";
|
||||
import Icons from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/unplugin-icons@0.18.5_@svgr+core@8.1.0/node_modules/unplugin-icons/dist/vite.js";
|
||||
import { defineConfig } from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/vite@5.1.2_@types+node@20.11.17_sass@1.70.0/node_modules/vite/dist/node/index.js";
|
||||
var __vite_injected_original_dirname = "C:\\Users\\somgu\\OneDrive\\Desktop\\UT-Registration-Plus\\.storybook";
|
||||
var root = resolve(__vite_injected_original_dirname, "../src");
|
||||
var pagesDir = resolve(root, "pages");
|
||||
var assetsDir = resolve(root, "assets");
|
||||
var publicDir = resolve(__vite_injected_original_dirname, "../public");
|
||||
console.log(root);
|
||||
var vite_storybook_config_default = defineConfig({
|
||||
plugins: [react(), UnoCSS(), Icons({ compiler: "jsx", jsx: "react" })],
|
||||
resolve: {
|
||||
alias: {
|
||||
src: root,
|
||||
"@assets": assetsDir,
|
||||
"@pages": pagesDir,
|
||||
"@public": publicDir,
|
||||
"@shared": resolve(root, "shared"),
|
||||
"@background": resolve(pagesDir, "background"),
|
||||
"@views": resolve(root, "views")
|
||||
}
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_storybook_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLnN0b3J5Ym9vay92aXRlLXN0b3J5Ym9vay5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFxzb21ndVxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXFVULVJlZ2lzdHJhdGlvbi1QbHVzXFxcXC5zdG9yeWJvb2tcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkM6XFxcXFVzZXJzXFxcXHNvbWd1XFxcXE9uZURyaXZlXFxcXERlc2t0b3BcXFxcVVQtUmVnaXN0cmF0aW9uLVBsdXNcXFxcLnN0b3J5Ym9va1xcXFx2aXRlLXN0b3J5Ym9vay5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L1VzZXJzL3NvbWd1L09uZURyaXZlL0Rlc2t0b3AvVVQtUmVnaXN0cmF0aW9uLVBsdXMvLnN0b3J5Ym9vay92aXRlLXN0b3J5Ym9vay5jb25maWcudHNcIjtpbXBvcnQgcmVhY3QgZnJvbSAnQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djJztcbmltcG9ydCB7IHJlc29sdmUgfSBmcm9tICdwYXRoJztcbmltcG9ydCBVbm9DU1MgZnJvbSAndW5vY3NzL3ZpdGUnO1xuaW1wb3J0IEljb25zIGZyb20gJ3VucGx1Z2luLWljb25zL3ZpdGUnO1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XG5cbmNvbnN0IHJvb3QgPSByZXNvbHZlKF9fZGlybmFtZSwgJy4uL3NyYycpO1xuY29uc3QgcGFnZXNEaXIgPSByZXNvbHZlKHJvb3QsICdwYWdlcycpO1xuY29uc3QgYXNzZXRzRGlyID0gcmVzb2x2ZShyb290LCAnYXNzZXRzJyk7XG5jb25zdCBwdWJsaWNEaXIgPSByZXNvbHZlKF9fZGlybmFtZSwgJy4uL3B1YmxpYycpO1xuXG5jb25zb2xlLmxvZyhyb290KTtcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gICAgcGx1Z2luczogW3JlYWN0KCksIFVub0NTUygpLCBJY29ucyh7IGNvbXBpbGVyOiAnanN4JywganN4OiAncmVhY3QnIH0pXSxcbiAgICByZXNvbHZlOiB7XG4gICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICBzcmM6IHJvb3QsXG4gICAgICAgICAgICAnQGFzc2V0cyc6IGFzc2V0c0RpcixcbiAgICAgICAgICAgICdAcGFnZXMnOiBwYWdlc0RpcixcbiAgICAgICAgICAgICdAcHVibGljJzogcHVibGljRGlyLFxuICAgICAgICAgICAgJ0BzaGFyZWQnOiByZXNvbHZlKHJvb3QsICdzaGFyZWQnKSxcbiAgICAgICAgICAgICdAYmFja2dyb3VuZCc6IHJlc29sdmUocGFnZXNEaXIsICdiYWNrZ3JvdW5kJyksXG4gICAgICAgICAgICAnQHZpZXdzJzogcmVzb2x2ZShyb290LCAndmlld3MnKSxcbiAgICAgICAgfSxcbiAgICB9LFxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWlaLE9BQU8sV0FBVztBQUNuYSxTQUFTLGVBQWU7QUFDeEIsT0FBTyxZQUFZO0FBQ25CLE9BQU8sV0FBVztBQUNsQixTQUFTLG9CQUFvQjtBQUo3QixJQUFNLG1DQUFtQztBQU16QyxJQUFNLE9BQU8sUUFBUSxrQ0FBVyxRQUFRO0FBQ3hDLElBQU0sV0FBVyxRQUFRLE1BQU0sT0FBTztBQUN0QyxJQUFNLFlBQVksUUFBUSxNQUFNLFFBQVE7QUFDeEMsSUFBTSxZQUFZLFFBQVEsa0NBQVcsV0FBVztBQUVoRCxRQUFRLElBQUksSUFBSTtBQUdoQixJQUFPLGdDQUFRLGFBQWE7QUFBQSxFQUN4QixTQUFTLENBQUMsTUFBTSxHQUFHLE9BQU8sR0FBRyxNQUFNLEVBQUUsVUFBVSxPQUFPLEtBQUssUUFBUSxDQUFDLENBQUM7QUFBQSxFQUNyRSxTQUFTO0FBQUEsSUFDTCxPQUFPO0FBQUEsTUFDSCxLQUFLO0FBQUEsTUFDTCxXQUFXO0FBQUEsTUFDWCxVQUFVO0FBQUEsTUFDVixXQUFXO0FBQUEsTUFDWCxXQUFXLFFBQVEsTUFBTSxRQUFRO0FBQUEsTUFDakMsZUFBZSxRQUFRLFVBQVUsWUFBWTtBQUFBLE1BQzdDLFVBQVUsUUFBUSxNQUFNLE9BQU87QUFBQSxJQUNuQztBQUFBLEVBQ0o7QUFDSixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
||||
8
.vscode/extensions.json
vendored
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"clinyong.vscode-css-modules"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"antfu.unocss",
|
||||
"editorconfig.editorconfig",
|
||||
"figma.figma-vscode-extension"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -37,7 +37,4 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"plaintext": "javascript"
|
||||
}
|
||||
}
|
||||
|
||||
13
@types/css-imports.d.ts
vendored
@@ -1,13 +0,0 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.mp3' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
22
@types/environment.d.ts
vendored
@@ -1,22 +0,0 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: 'development' | 'production';
|
||||
CI?: string;
|
||||
/** set this to make sure the extension id is the same for unpacked extensions
|
||||
* @see https://developer.chrome.com/docs/apps/app_identity/#copy_key */
|
||||
MANIFEST_KEY?: string;
|
||||
/**
|
||||
* The Node semantic versioning-compatible version of the extension. For preview-style releases, this variable
|
||||
* converts versions like 1.0.0.100 to 1.0.0-beta.1.
|
||||
*/
|
||||
SEMANTIC_VERSION?: string;
|
||||
}
|
||||
}
|
||||
|
||||
type Environment = typeof process.env.NODE_ENV;
|
||||
}
|
||||
|
||||
// If this file has no import/export statements (i.e. is a script)
|
||||
// convert it into a module by adding an empty export statement.
|
||||
export {};
|
||||
6
@types/svg-import.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
declare module "*.svg" {
|
||||
import { ReactElement, SVGProps } from "react";
|
||||
|
||||
const ReactComponent: (props: SVGProps<SVGElement>) => ReactElement;
|
||||
export default ReactComponent;
|
||||
}
|
||||
10
@types/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_PACKAGE_VERSION: string;
|
||||
readonly VITE_BETA_BUILD?: 'true';
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
63
README.md
@@ -1,30 +1,59 @@
|
||||
# UT Registration Plus
|
||||
|
||||
## Built Using
|
||||
We've all been there. 20 tabs of Rate My Professor, Google Spreadsheet, and the UT Course Schedule open and you still don't know what classes to take.
|
||||
This extension, UT Registration Plus (UTRP), tries to streamline most of the unnecessary steps and headaches of registering for classes at UT Austin.
|
||||
|
||||
- React 18
|
||||
- TypeScript
|
||||
- Vite 5
|
||||
- ESLint
|
||||
- Prettier
|
||||
- Semantic-Release
|
||||
- Custom Messaging & Storage Wrappers
|
||||
- For each class in the UT Course Schedule site, UTRP provides a "breakdown" popup, with quick and easy links to the instructor's RateMyProfessor, Course Evaluation Survey (CES) and past syllabi.
|
||||
|
||||
## Getting Started
|
||||
- Shows the course description with highlighted information on prerequisites, restrictions, etc.
|
||||
|
||||
- Shows an aggregate and semesterly graph of the grade distributions for each course.
|
||||
|
||||
- Gives you the ability to add "Add Course" and view them in the extension popup, a quick list of all the courses you have saved and an easy way to copy unique numbers.
|
||||
|
||||
- Highlights and crosses-out what courses on the UT Course Catalog would conflict with your currently saved courses, making selecting courses that fit with your schedule so much easier.
|
||||
|
||||
- Display's a weekly schedule based on your saved courses.
|
||||
|
||||
- Give you the ability to create multiple schedules to plan for different scenarios.
|
||||
|
||||
- ... and much more!
|
||||
|
||||
## Toolchain
|
||||
|
||||
- React 18
|
||||
- TypeScript
|
||||
- Vite 5
|
||||
- ESLint
|
||||
- Prettier
|
||||
- Storybook
|
||||
- Semantic-Release
|
||||
- Custom Messaging & Storage Wrappers
|
||||
|
||||
## Development: Getting Started
|
||||
|
||||
1. Clone this repo
|
||||
2. Run `pnpm install` to install and patch all the required dependencies
|
||||
2. This project uses `pnpm` to manage and patch dependencies. Run `pnpm install` to configure the repository for building/development
|
||||
3. Using either of the methods listed below, the extension will build to the `dist/` directory.
|
||||
|
||||
- If you want to run the development build:
|
||||
### Development Builds
|
||||
|
||||
- Run `pnpm run dev`
|
||||
- Run `pnpm dev`
|
||||
|
||||
- If you want to build the extension for production:
|
||||
> [!NOTE]
|
||||
> Injected content such as extension content on UT pages is not properly styled, and are missing class stylings. When developing for these pages, use `NODE_ENV='development' pnpm run dev build --mode development -w` to build and watch for changes. This will ensure you are seeing an accurate representation of the extension.
|
||||
|
||||
- Run `pnpm build`
|
||||
### Production Builds
|
||||
|
||||
You may have to rename the `__uno.css.js` to `uno.css.js` in dist
|
||||
- Run `pnpm build`
|
||||
|
||||
Go to chrome://extensions, ensure you have "Developer Mode" enabled, and click 'Load unpacked'
|
||||
<details>
|
||||
<summary>Beta builds</summary>
|
||||
Use `BETA=true pnpm build` to build a beta build.
|
||||
</details>
|
||||
|
||||
Navigate to the 'dist' folder and click 'select' to import the extension
|
||||
## Development: Loading the Extension Manually
|
||||
|
||||
Open [chrome://extensions](chrome://extensions), ensure you have 'Developer Mode' enabled, and click 'Load unpacked'.
|
||||
|
||||
Navigate to the `dist/` folder, and click 'select' to import the extension.
|
||||
|
||||
5
chromatic.config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"onlyChanged": true,
|
||||
"projectId": "Project:65c5172964f36dcf207985bf",
|
||||
"zip": true
|
||||
}
|
||||
23602
package-lock.json
generated
100
package.json
@@ -1,110 +1,111 @@
|
||||
{
|
||||
"name": "ut-registration-plus",
|
||||
"displayName": "UT Registration Plus",
|
||||
"version": "0.0.1",
|
||||
"description": "The UT Registration Plus extension is a Chrome extension that allows students to easily register for classes at The University of Texas at Austin.",
|
||||
"version": "2.0.0-beta4",
|
||||
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
|
||||
"private": true,
|
||||
"homepage": "sriramhariharan.com",
|
||||
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"prettier": "prettier src --check",
|
||||
"prettier:fix": "prettier src --write",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
|
||||
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"coverage": "vitest run --coverage",
|
||||
"preview": "vite preview",
|
||||
"devtools": "react-devtools",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@headlessui/react": "^2.0.3",
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@unocss/vite": "^0.58.6",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"chrome-extension-toolkit": "^0.0.51",
|
||||
"chrome-extension-toolkit": "^0.0.54",
|
||||
"clsx": "^2.1.0",
|
||||
"highcharts": "^11.3.0",
|
||||
"highcharts-react-official": "^3.2.1",
|
||||
"html-to-image": "^1.11.11",
|
||||
"react": "^18.2.0",
|
||||
"react-devtools-core": "^5.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-window": "^1.8.10",
|
||||
"husky": "^9.0.11",
|
||||
"nanoid": "^5.0.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.71.1",
|
||||
"sql.js": "1.10.2",
|
||||
"styled-components": "^6.1.8",
|
||||
"uuid": "^9.0.1"
|
||||
"sql.js": "1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.4.0",
|
||||
"@commitlint/cli": "^18.6.1",
|
||||
"@commitlint/config-conventional": "^18.6.2",
|
||||
"@commitlint/types": "^19.0.3",
|
||||
"@crxjs/vite-plugin": "2.0.0-beta.21",
|
||||
"@iconify-json/bi": "^1.1.23",
|
||||
"@iconify-json/material-symbols": "^1.1.73",
|
||||
"@storybook/addon-designs": "^7.0.9",
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-links": "^7.6.17",
|
||||
"@storybook/blocks": "^7.6.17",
|
||||
"@storybook/react": "^7.6.17",
|
||||
"@storybook/react-vite": "^7.6.17",
|
||||
"@storybook/test": "^7.6.17",
|
||||
"@iconify-json/ri": "^1.1.20",
|
||||
"@storybook/addon-designs": "^8.0.1",
|
||||
"@storybook/addon-essentials": "^8.1.1",
|
||||
"@storybook/addon-links": "^8.1.1",
|
||||
"@storybook/blocks": "^8.1.1",
|
||||
"@storybook/react": "^8.1.1",
|
||||
"@storybook/react-vite": "^8.1.1",
|
||||
"@storybook/test": "^8.1.1",
|
||||
"@svgr/core": "^8.1.0",
|
||||
"@svgr/plugin-jsx": "^8.1.0",
|
||||
"@types/chrome": "^0.0.260",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^18.2.57",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/semver": "^7.5.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@unocss/eslint-config": "^0.58.5",
|
||||
"@unocss/postcss": "^0.58.5",
|
||||
"@unocss/preset-uno": "^0.58.5",
|
||||
"@unocss/preset-web-fonts": "^0.58.5",
|
||||
"@unocss/reset": "^0.58.5",
|
||||
"@unocss/transformer-directives": "^0.58.5",
|
||||
"@unocss/transformer-variant-group": "^0.58.5",
|
||||
"@unocss/eslint-config": "^0.58.6",
|
||||
"@unocss/postcss": "^0.58.6",
|
||||
"@unocss/preset-uno": "^0.58.6",
|
||||
"@unocss/preset-web-fonts": "^0.58.6",
|
||||
"@unocss/reset": "^0.58.6",
|
||||
"@unocss/transformer-directives": "^0.58.6",
|
||||
"@unocss/transformer-variant-group": "^0.58.6",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"@vitest/coverage-v8": "^1.3.1",
|
||||
"@vitest/ui": "^1.3.1",
|
||||
"chromatic": "^10.9.6",
|
||||
"cssnano": "^6.0.3",
|
||||
"cssnano-preset-advanced": "^6.0.3",
|
||||
"chromatic": "^11.3.5",
|
||||
"cssnano": "^6.0.5",
|
||||
"cssnano-preset-advanced": "^6.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-module-lexer": "^1.4.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.1.0",
|
||||
"eslint-plugin-import-essentials": "^0.2.1",
|
||||
"eslint-plugin-jsdoc": "^48.2.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"husky": "^9.0.11",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"path": "^0.12.7",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-devtools": "^5.0.0",
|
||||
"storybook": "^7.6.17",
|
||||
"typescript": "^5.3.3",
|
||||
"unocss": "^0.58.5",
|
||||
"storybook": "^8.1.1",
|
||||
"typescript": "^5.4.3",
|
||||
"unocss": "^0.58.6",
|
||||
"unocss-preset-primitives": "0.0.2-beta.0",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-inspect": "^0.8.3",
|
||||
@@ -112,7 +113,8 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch"
|
||||
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch",
|
||||
"@unocss/vite@0.58.6": "patches/@unocss__vite@0.58.6.patch"
|
||||
},
|
||||
"overrides": {
|
||||
"es-module-lexer": "^1.4.1"
|
||||
|
||||
108
patches/@unocss__vite@0.58.6.patch
Normal file
@@ -0,0 +1,108 @@
|
||||
diff --git a/dist/index.cjs b/dist/index.cjs
|
||||
index 560f423a07f21b0c47abd494d77654de4c874481..35ae1fdca8bd5546f7e40a23edacb1dbbbd34b58 100644
|
||||
--- a/dist/index.cjs
|
||||
+++ b/dist/index.cjs
|
||||
@@ -35,15 +35,15 @@ const VIRTUAL_ENTRY_ALIAS = [
|
||||
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
|
||||
];
|
||||
const LAYER_MARK_ALL = "__ALL__";
|
||||
-const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]__uno(?:(_.*?))?\.css(\?.*)?$/;
|
||||
-const RESOLVED_ID_RE = /[\/\\]__uno(?:_(.*?))?\.css$/;
|
||||
+const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]uno(?:(_.*?))?\.css(\?.*)?$/;
|
||||
+const RESOLVED_ID_RE = /[\/\\]uno(?:_(.*?))?\.css$/;
|
||||
function resolveId(id) {
|
||||
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
|
||||
return id;
|
||||
for (const alias of VIRTUAL_ENTRY_ALIAS) {
|
||||
const match = id.match(alias);
|
||||
if (match) {
|
||||
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
|
||||
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,7 +745,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
|
||||
const { hash, css } = await generateCSS(layer);
|
||||
return {
|
||||
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
|
||||
- code: `__uno_hash_${hash}{--:'';}${css}`,
|
||||
+ code: `uno_hash_${hash}{--:'';}${css}`,
|
||||
map: { mappings: "" }
|
||||
};
|
||||
},
|
||||
@@ -764,7 +764,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
|
||||
if (layer && code.includes("import.meta.hot")) {
|
||||
let hmr = `
|
||||
try {
|
||||
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
|
||||
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
|
||||
hash = hash && hash[1]
|
||||
if (!hash)
|
||||
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index bbbccb7cad7421cbdb97223a451ec5853c0476cb..4bf6a08d94e562090a530308c0ab8337afdf8243 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -1,17 +1,17 @@
|
||||
-import process$1 from 'node:process';
|
||||
+import remapping from '@ampproject/remapping';
|
||||
+import { createFilter } from '@rollup/pluginutils';
|
||||
+import { loadConfig } from '@unocss/config';
|
||||
+import { BetterMap, createGenerator, cssIdRE, notNull, toEscapedSelector } from '@unocss/core';
|
||||
import UnocssInspector from '@unocss/inspector';
|
||||
-import { resolve, isAbsolute, dirname } from 'node:path';
|
||||
-import fs from 'node:fs/promises';
|
||||
import fg from 'fast-glob';
|
||||
import MagicString from 'magic-string';
|
||||
-import remapping from '@ampproject/remapping';
|
||||
-import { createHash } from 'node:crypto';
|
||||
-import { cssIdRE, createGenerator, BetterMap, notNull, toEscapedSelector } from '@unocss/core';
|
||||
import { Buffer } from 'node:buffer';
|
||||
-import { createFilter } from '@rollup/pluginutils';
|
||||
+import { createHash } from 'node:crypto';
|
||||
import fs$1 from 'node:fs';
|
||||
+import fs from 'node:fs/promises';
|
||||
+import { dirname, isAbsolute, resolve } from 'node:path';
|
||||
+import process$1 from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
-import { loadConfig } from '@unocss/config';
|
||||
|
||||
const defaultPipelineExclude = [cssIdRE];
|
||||
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/];
|
||||
@@ -20,15 +20,15 @@ const VIRTUAL_ENTRY_ALIAS = [
|
||||
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
|
||||
];
|
||||
const LAYER_MARK_ALL = "__ALL__";
|
||||
-const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]__uno(?:(_.*?))?\.css(\?.*)?$/;
|
||||
-const RESOLVED_ID_RE = /[\/\\]__uno(?:_(.*?))?\.css$/;
|
||||
+const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]uno(?:(_.*?))?\.css(\?.*)?$/;
|
||||
+const RESOLVED_ID_RE = /[\/\\]uno(?:_(.*?))?\.css$/;
|
||||
function resolveId(id) {
|
||||
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
|
||||
return id;
|
||||
for (const alias of VIRTUAL_ENTRY_ALIAS) {
|
||||
const match = id.match(alias);
|
||||
if (match) {
|
||||
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
|
||||
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,7 +730,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
|
||||
const { hash, css } = await generateCSS(layer);
|
||||
return {
|
||||
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
|
||||
- code: `__uno_hash_${hash}{--:'';}${css}`,
|
||||
+ code: `uno_hash_${hash}{--:'';}${css}`,
|
||||
map: { mappings: "" }
|
||||
};
|
||||
},
|
||||
@@ -749,7 +749,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
|
||||
if (layer && code.includes("import.meta.hot")) {
|
||||
let hmr = `
|
||||
try {
|
||||
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
|
||||
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
|
||||
hash = hash && hash[1]
|
||||
if (!hash)
|
||||
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')
|
||||
20041
pnpm-lock.yaml
generated
BIN
public/database/grade_distributions.db
Normal file
30
public/icons/icon_beta.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Beta" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-width: 70.06px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #005f86;
|
||||
}
|
||||
|
||||
.cls-2, .cls-3 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="cls-2" width="1024" height="1024"/>
|
||||
<g>
|
||||
<circle class="cls-1" cx="512" cy="512" r="362"/>
|
||||
<rect class="cls-3" x="466.29" y="283.46" width="91.41" height="457.07"/>
|
||||
<rect class="cls-3" x="283.46" y="466.29" width="457.07" height="91.41"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 684 B |
BIN
public/icons/icon_beta_128.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icons/icon_beta_16.png
Normal file
|
After Width: | Height: | Size: 429 B |
BIN
public/icons/icon_beta_32.png
Normal file
|
After Width: | Height: | Size: 791 B |
BIN
public/icons/icon_beta_48.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
30
public/icons/icon_development.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Development" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-width: 70.06px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #bf2178;
|
||||
}
|
||||
|
||||
.cls-2, .cls-3 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="cls-2" x="0" width="1024" height="1024"/>
|
||||
<g>
|
||||
<circle class="cls-1" cx="512" cy="512" r="362"/>
|
||||
<rect class="cls-3" x="466.29" y="283.46" width="91.41" height="457.07"/>
|
||||
<rect class="cls-3" x="283.46" y="466.29" width="457.07" height="91.41"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 697 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 449 B |
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
30
public/icons/icon_production.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Production" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #bf5700;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-width: 70.06px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="cls-1" width="1024" height="1024"/>
|
||||
<g>
|
||||
<circle class="cls-3" cx="512" cy="512" r="362"/>
|
||||
<rect class="cls-2" x="466.29" y="283.46" width="91.41" height="457.07"/>
|
||||
<rect class="cls-2" x="283.46" y="466.29" width="457.07" height="91.41"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 706 B |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_3175_7842" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
|
||||
<rect y="0.5" width="24" height="24" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3175_7842)">
|
||||
<path d="M12 14.5C11.7167 14.5 11.4792 14.4042 11.2875 14.2125C11.0958 14.0208 11 13.7833 11 13.5C11 13.2167 11.0958 12.9792 11.2875 12.7875C11.4792 12.5958 11.7167 12.5 12 12.5C12.2833 12.5 12.5208 12.5958 12.7125 12.7875C12.9042 12.9792 13 13.2167 13 13.5C13 13.7833 12.9042 14.0208 12.7125 14.2125C12.5208 14.4042 12.2833 14.5 12 14.5ZM8 14.5C7.71667 14.5 7.47917 14.4042 7.2875 14.2125C7.09583 14.0208 7 13.7833 7 13.5C7 13.2167 7.09583 12.9792 7.2875 12.7875C7.47917 12.5958 7.71667 12.5 8 12.5C8.28333 12.5 8.52083 12.5958 8.7125 12.7875C8.90417 12.9792 9 13.2167 9 13.5C9 13.7833 8.90417 14.0208 8.7125 14.2125C8.52083 14.4042 8.28333 14.5 8 14.5ZM16 14.5C15.7167 14.5 15.4792 14.4042 15.2875 14.2125C15.0958 14.0208 15 13.7833 15 13.5C15 13.2167 15.0958 12.9792 15.2875 12.7875C15.4792 12.5958 15.7167 12.5 16 12.5C16.2833 12.5 16.5208 12.5958 16.7125 12.7875C16.9042 12.9792 17 13.2167 17 13.5C17 13.7833 16.9042 14.0208 16.7125 14.2125C16.5208 14.4042 16.2833 14.5 16 14.5ZM12 18.5C11.7167 18.5 11.4792 18.4042 11.2875 18.2125C11.0958 18.0208 11 17.7833 11 17.5C11 17.2167 11.0958 16.9792 11.2875 16.7875C11.4792 16.5958 11.7167 16.5 12 16.5C12.2833 16.5 12.5208 16.5958 12.7125 16.7875C12.9042 16.9792 13 17.2167 13 17.5C13 17.7833 12.9042 18.0208 12.7125 18.2125C12.5208 18.4042 12.2833 18.5 12 18.5ZM8 18.5C7.71667 18.5 7.47917 18.4042 7.2875 18.2125C7.09583 18.0208 7 17.7833 7 17.5C7 17.2167 7.09583 16.9792 7.2875 16.7875C7.47917 16.5958 7.71667 16.5 8 16.5C8.28333 16.5 8.52083 16.5958 8.7125 16.7875C8.90417 16.9792 9 17.2167 9 17.5C9 17.7833 8.90417 18.0208 8.7125 18.2125C8.52083 18.4042 8.28333 18.5 8 18.5ZM16 18.5C15.7167 18.5 15.4792 18.4042 15.2875 18.2125C15.0958 18.0208 15 17.7833 15 17.5C15 17.2167 15.0958 16.9792 15.2875 16.7875C15.4792 16.5958 15.7167 16.5 16 16.5C16.2833 16.5 16.5208 16.5958 16.7125 16.7875C16.9042 16.9792 17 17.2167 17 17.5C17 17.7833 16.9042 18.0208 16.7125 18.2125C16.5208 18.4042 16.2833 18.5 16 18.5ZM5 22.5C4.45 22.5 3.97917 22.3042 3.5875 21.9125C3.19583 21.5208 3 21.05 3 20.5V6.5C3 5.95 3.19583 5.47917 3.5875 5.0875C3.97917 4.69583 4.45 4.5 5 4.5H6V2.5H8V4.5H16V2.5H18V4.5H19C19.55 4.5 20.0208 4.69583 20.4125 5.0875C20.8042 5.47917 21 5.95 21 6.5V20.5C21 21.05 20.8042 21.5208 20.4125 21.9125C20.0208 22.3042 19.55 22.5 19 22.5H5ZM5 20.5H19V10.5H5V20.5Z" fill="#333F48"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_3211_5369" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
|
||||
<rect y="0.5" width="24" height="24" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3211_5369)">
|
||||
<path d="M5 21.5C4.45 21.5 3.97917 21.3042 3.5875 20.9125C3.19583 20.5208 3 20.05 3 19.5V5.5C3 4.95 3.19583 4.47917 3.5875 4.0875C3.97917 3.69583 4.45 3.5 5 3.5H19C19.55 3.5 20.0208 3.69583 20.4125 4.0875C20.8042 4.47917 21 4.95 21 5.5V19.5C21 20.05 20.8042 20.5208 20.4125 20.9125C20.0208 21.3042 19.55 21.5 19 21.5H5ZM6 17.5H18L14.25 12.5L11.25 16.5L9 13.5L6 17.5Z" fill="#333F48"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 702 B |
54
src/assets/insideJokes.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
const splashText: string[] = [
|
||||
"You can't fail classes you're not in, that's for sure.",
|
||||
'Steer clear of O-Chem, unless you fancy a challenge.',
|
||||
'Rec Sports fills up fast, even before the sun reaches its peak.',
|
||||
"Ah, Jendy's! A taste ever so refined.",
|
||||
'Fine dining at Jester City Limits, eh?',
|
||||
'Rec Sports fills up fast, even before the sun reaches its peak.',
|
||||
'RIP Domino, you beloved campus feline.',
|
||||
"The year is 2055 and Welch still isn't finished.",
|
||||
'Motivation dropping faster than ur GPA',
|
||||
'No Work Happens On PCL 5th Floor.',
|
||||
'I may be a sophomore in name, but my credit count screams freshman!',
|
||||
'Pain is temporary, GPA is forever.',
|
||||
"You've Yee'd Your Last Haw.",
|
||||
'lol everything is already waitlisted.',
|
||||
"At Least You're Not At A&M.",
|
||||
'TeXAs iS BaCK GuYZ',
|
||||
'mAke iT yOuR tExAS',
|
||||
"'Academically Challenged'",
|
||||
'Does McCombs teach Parseltongue?',
|
||||
'No Cruce Enfrente Del Bus.',
|
||||
'Omae Wa Mou Shindeiru...',
|
||||
'Bevo Bucks are the new Bitcoin',
|
||||
'Every day another brick disappears from Speedway',
|
||||
'The GDC will annex the EER one day',
|
||||
'Just you wait. Our CNS operatives will topple the EER regime',
|
||||
'To hike to Kins or not to hike to Kins...',
|
||||
'The road to Kinsolving is long, but their delicacies makes it worth every step.',
|
||||
"C'mon you Longhorns! You want to study forever?",
|
||||
'HOW BOUT A NICE CUP OF LIBER TEA',
|
||||
"It's called the quiet floor of the PCL for a reason",
|
||||
"'Whose car is this and why is it attempting to enter Welch?'",
|
||||
"'I really like one of my TAs and I wanna ask her out after this semester ends'",
|
||||
'CaN YoU aSk OuT a tA aFtEr tHe SeMeStEr Is oVeR AnD gRaDeS ArE DoNe?',
|
||||
"The Block of Butter incident of '22",
|
||||
"Arrows of Christ vs Church of Scientology was the crossover we didn't know we needed",
|
||||
'THE WALK SIGN IS ON TO CROSS GUADALUPE AND 21ST',
|
||||
'Days since last GDC door alarm: 0',
|
||||
'Finding a parking spot is like winning the lottery... if the lottery required parallel parking skills.',
|
||||
'The squirrels are more ambitious than most freshmen during finals week.',
|
||||
'The line at the on-campus Starbucks is longer than your course waitlist.',
|
||||
'The weather changes more often than your class schedule.',
|
||||
"'Sorry, the PCL is full' is the most heartbreaking message you'll ever receive.",
|
||||
'Getting to class on time is like navigating a maze of construction zones.',
|
||||
"'studying' often means refreshing Canvas every five minutes to see if the professor posted lecture slides.",
|
||||
"'I'll just skip this lecture' often turns into a semester-long habit.",
|
||||
'The libraries are full of students pretending to study but actually napping with their eyes open.',
|
||||
"The 'campus loop' refers to both the bus route and the endless cycle of trying to find your way around campus.",
|
||||
'The squirrels have mastered the art of begging for food better than most students during finals week.',
|
||||
"'going for a run' often means 'running to get food from food trucks'.",
|
||||
'The struggle to fit all your classes into one schedule is as real as the struggle to fit all your groceries into a mini-fridge.',
|
||||
];
|
||||
|
||||
export default splashText;
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -4,9 +4,19 @@ import { createRoot } from 'react-dom/client';
|
||||
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
|
||||
/**
|
||||
* Handles editing the storage for a specific area.
|
||||
*
|
||||
* @param {string} areaName - The name of the storage area.
|
||||
* @returns {Function} - A function that accepts changes and sets them in the storage.
|
||||
*/
|
||||
const handleEditStorage = (areaName: 'local' | 'sync' | 'session') => (changes: Record<string, unknown>) => {
|
||||
chrome.storage[areaName].set(changes);
|
||||
};
|
||||
|
||||
interface JSONEditorProps {
|
||||
data: any;
|
||||
onChange: (updates: any) => void;
|
||||
data: unknown;
|
||||
onChange: ReturnType<typeof handleEditStorage>;
|
||||
}
|
||||
|
||||
function JSONEditor(props: JSONEditorProps) {
|
||||
@@ -29,6 +39,8 @@ function JSONEditor(props: JSONEditorProps) {
|
||||
setIsEditing(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
alert('Invalid JSON');
|
||||
}
|
||||
};
|
||||
@@ -62,9 +74,9 @@ function JSONEditor(props: JSONEditorProps) {
|
||||
// ));
|
||||
|
||||
function DevDashboard() {
|
||||
const [localStorage, setLocalStorage] = React.useState<any>({});
|
||||
const [syncStorage, setSyncStorage] = React.useState<any>({});
|
||||
const [sessionStorage, setSessionStorage] = React.useState<any>({});
|
||||
const [localStorage, setLocalStorage] = React.useState<Record<string, unknown>>({});
|
||||
const [syncStorage, setSyncStorage] = React.useState<Record<string, unknown>>({});
|
||||
const [sessionStorage, setSessionStorage] = React.useState<Record<string, unknown>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const onVisibilityChange = () => {
|
||||
@@ -93,7 +105,8 @@ function DevDashboard() {
|
||||
// listen for changes to the chrome storage to update the local storage state displayed in the dashboard
|
||||
useEffect(() => {
|
||||
const onChanged = (changes: chrome.storage.StorageChange, areaName: chrome.storage.AreaName) => {
|
||||
let copy = {};
|
||||
let copy: Record<string, unknown> = {};
|
||||
|
||||
if (areaName === 'local') {
|
||||
copy = { ...localStorage };
|
||||
} else if (areaName === 'sync') {
|
||||
@@ -102,8 +115,8 @@ function DevDashboard() {
|
||||
copy = { ...sessionStorage };
|
||||
}
|
||||
|
||||
Object.keys(changes).forEach(key => {
|
||||
copy[key] = changes[key].newValue;
|
||||
Object.keys(changes).forEach((key: string) => {
|
||||
copy[key] = changes[key as keyof typeof changes].newValue;
|
||||
});
|
||||
|
||||
if (areaName === 'local') {
|
||||
@@ -124,10 +137,6 @@ function DevDashboard() {
|
||||
};
|
||||
}, [localStorage, syncStorage, sessionStorage]);
|
||||
|
||||
const handleEditStorage = (areaName: string) => (changes: Record<string, any>) => {
|
||||
chrome.storage[areaName].set(changes);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
@@ -145,4 +154,4 @@ function DevDashboard() {
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')).render(<DevDashboard />);
|
||||
createRoot(document.getElementById('root')!).render(<DevDashboard />);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// this is a custom wrapper around react-devtools
|
||||
// that changes it so that we only send messages to the devtools when the current tab is active;
|
||||
import { connectToDevTools } from 'react-devtools-core';
|
||||
|
||||
// connect to the devtools server
|
||||
let ws = new WebSocket('ws://localhost:8097');
|
||||
|
||||
connectToDevTools({
|
||||
websocket: ws,
|
||||
});
|
||||
|
||||
// when the tab's visibile state changes, we connect or disconnect from the devtools
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
ws = new WebSocket('ws://localhost:8097');
|
||||
connectToDevTools({
|
||||
websocket: ws,
|
||||
});
|
||||
} else {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineManifest } from '@crxjs/vite-plugin';
|
||||
|
||||
import packageJson from '../package.json';
|
||||
|
||||
// Convert from Semver (example: 0.1.0-beta6)
|
||||
@@ -8,7 +9,12 @@ const [major, minor, patch, label = '0'] = packageJson.version
|
||||
// split into version parts
|
||||
.split(/[.-]/);
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const isBeta = !!process.env.BETA;
|
||||
const mode = isBeta ? 'beta' : process.env.NODE_ENV;
|
||||
|
||||
if (isBeta && process.env.NODE_ENV !== 'production') throw new Error('Cannot have beta non-production build');
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const nameSuffix = isBeta ? ' (beta)' : mode === 'development' ? ' (dev)' : '';
|
||||
|
||||
const HOST_PERMISSIONS: string[] = [
|
||||
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
|
||||
@@ -16,16 +22,17 @@ const HOST_PERMISSIONS: string[] = [
|
||||
'*://*.catalog.utexas.edu/ribbit/',
|
||||
'*://*.registrar.utexas.edu/schedules/*',
|
||||
'*://*.login.utexas.edu/login/*',
|
||||
'https://utexas.bluera.com/*',
|
||||
];
|
||||
|
||||
const manifest = defineManifest(async () => ({
|
||||
manifest_version: 3,
|
||||
name: `${packageJson.displayName ?? packageJson.name}${mode === 'development' ? ' (dev)' : ''}`,
|
||||
name: `${packageJson.displayName ?? packageJson.name}${nameSuffix}`,
|
||||
version: `${major}.${minor}.${patch}.${label}`,
|
||||
description: packageJson.description,
|
||||
options_page: 'src/pages/options/index.html',
|
||||
background: { service_worker: 'src/pages/background/background.ts' },
|
||||
permissions: ['storage', 'unlimitedStorage', 'background'],
|
||||
permissions: ['storage', 'unlimitedStorage', 'background', 'scripting'],
|
||||
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
|
||||
action: {
|
||||
default_popup: 'src/pages/popup/index.html',
|
||||
@@ -49,6 +56,9 @@ const manifest = defineManifest(async () => ({
|
||||
matches: ['*://*/*'],
|
||||
},
|
||||
],
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
||||
},
|
||||
}));
|
||||
|
||||
export default manifest;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { BACKGROUND_MESSAGES } from '@shared/messages';
|
||||
import type { BACKGROUND_MESSAGES } from '@shared/messages';
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
import updateBadgeText from '@shared/util/updateBadgeText';
|
||||
import { MessageListener } from 'chrome-extension-toolkit';
|
||||
|
||||
import onInstall from './events/onInstall';
|
||||
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
|
||||
import onUpdate from './events/onUpdate';
|
||||
import browserActionHandler from './handler/browserActionHandler';
|
||||
import calendarBackgroundHandler from './handler/calendarBackgroundHandler';
|
||||
import CESHandler from './handler/CESHandler';
|
||||
import tabManagementHandler from './handler/tabManagementHandler';
|
||||
import userScheduleHandler from './handler/userScheduleHandler';
|
||||
|
||||
@@ -31,6 +36,24 @@ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
|
||||
...browserActionHandler,
|
||||
...tabManagementHandler,
|
||||
...userScheduleHandler,
|
||||
...CESHandler,
|
||||
...calendarBackgroundHandler,
|
||||
});
|
||||
|
||||
messageListener.listen();
|
||||
|
||||
UserScheduleStore.listen('schedules', async schedules => {
|
||||
const index = await UserScheduleStore.get('activeIndex');
|
||||
const numCourses = schedules.newValue[index]?.courses?.length;
|
||||
if (!numCourses) return;
|
||||
|
||||
updateBadgeText(numCourses);
|
||||
});
|
||||
|
||||
UserScheduleStore.listen('activeIndex', async ({ newValue }) => {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
const numCourses = schedules[newValue]?.courses?.length;
|
||||
if (!numCourses) return;
|
||||
|
||||
updateBadgeText(numCourses);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { openDebugTab } from '../util/openDebugTab';
|
||||
import { openDebugTab } from '@pages/background/util/openDebugTab';
|
||||
|
||||
/**
|
||||
* Called whenever the background service worker comes alive
|
||||
|
||||
39
src/pages/background/handler/CESHandler.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import openNewTab from '@background/util/openNewTab';
|
||||
import type CESMessage from '@shared/messages/CESMessage';
|
||||
import type { MessageHandler } from 'chrome-extension-toolkit';
|
||||
|
||||
const CESFall2023Url = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580®l=en-US';
|
||||
|
||||
const CESHandler: MessageHandler<CESMessage> = {
|
||||
openCESPage({ data, sendResponse }) {
|
||||
const { instructorFirstName, instructorLastName } = data;
|
||||
openNewTab(CESFall2023Url).then(tab => {
|
||||
const instructorFirstAndLastName = [instructorFirstName, instructorLastName];
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: (...instructorFirstAndLastName: string[]) => {
|
||||
const inputElement = document.getElementById(
|
||||
'ctl00_ContentPlaceHolder1_ViewList_tbxValue'
|
||||
) as HTMLInputElement | null;
|
||||
const [instructorFirstName, instructorLastName] = instructorFirstAndLastName;
|
||||
if (inputElement) {
|
||||
inputElement.value = `${instructorFirstName} ${instructorLastName}`;
|
||||
inputElement.focus();
|
||||
const enterKeyEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
});
|
||||
inputElement.dispatchEvent(enterKeyEvent);
|
||||
}
|
||||
},
|
||||
args: instructorFirstAndLastName,
|
||||
});
|
||||
sendResponse(tab);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default CESHandler;
|
||||
@@ -1,5 +1,5 @@
|
||||
import BrowserActionMessages from '@shared/messages/BrowserActionMessages';
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
import type BrowserActionMessages from '@shared/messages/BrowserActionMessages';
|
||||
import type { MessageHandler } from 'chrome-extension-toolkit';
|
||||
|
||||
const browserActionHandler: MessageHandler<BrowserActionMessages> = {
|
||||
disableBrowserAction({ sender, sendResponse }) {
|
||||
|
||||
48
src/pages/background/handler/calendarBackgroundHandler.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { TabWithId } from '@background/util/openNewTab';
|
||||
import openNewTab from '@background/util/openNewTab';
|
||||
import { tabs } from '@shared/messages';
|
||||
import type { CalendarBackgroundMessages } from '@shared/messages/CalendarMessages';
|
||||
import type { MessageHandler } from 'chrome-extension-toolkit';
|
||||
|
||||
const getAllTabInfos = async () => {
|
||||
const openTabs = (await chrome.tabs.query({})).filter((tab): tab is TabWithId => tab.id !== undefined);
|
||||
const results = await Promise.allSettled(openTabs.map(tab => tabs.getTabInfo(undefined, tab.id)));
|
||||
|
||||
type TabInfo = PromiseFulfilledResult<Awaited<ReturnType<typeof tabs.getTabInfo>>>;
|
||||
return results
|
||||
.map((result, index) => ({ result, index }))
|
||||
.filter((el): el is { result: TabInfo; index: number } => el.result.status === 'fulfilled')
|
||||
.map(({ result, index }) => ({
|
||||
...result.value,
|
||||
tab: openTabs[index]!,
|
||||
}));
|
||||
};
|
||||
|
||||
const calendarBackgroundHandler: MessageHandler<CalendarBackgroundMessages> = {
|
||||
async switchToCalendarTab({ data, sendResponse }) {
|
||||
const { uniqueId } = data;
|
||||
const calendarUrl = chrome.runtime.getURL(`calendar.html`);
|
||||
|
||||
const allTabs = await getAllTabInfos();
|
||||
|
||||
const openCalendarTabInfo = allTabs.find(tab => tab.url?.startsWith(calendarUrl));
|
||||
|
||||
if (openCalendarTabInfo !== undefined) {
|
||||
const tabid = openCalendarTabInfo.tab.id;
|
||||
|
||||
chrome.tabs.update(tabid, { active: true });
|
||||
if (uniqueId !== undefined) await tabs.openCoursePopup({ uniqueId }, tabid);
|
||||
|
||||
sendResponse(openCalendarTabInfo.tab);
|
||||
} else {
|
||||
const urlParams = new URLSearchParams();
|
||||
if (uniqueId !== undefined) urlParams.set('uniqueId', uniqueId.toString());
|
||||
const url = `${calendarUrl}?${urlParams.toString()}`.replace(/\?$/, '');
|
||||
const tab = await openNewTab(url);
|
||||
|
||||
sendResponse(tab);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default calendarBackgroundHandler;
|
||||
@@ -1,24 +0,0 @@
|
||||
import HotReloadingMessages from '@shared/messages/HotReloadingMessages';
|
||||
import { DevStore } from '@shared/storage/DevStore';
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
|
||||
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
|
||||
async reloadExtension({ sendResponse }) {
|
||||
const [isExtensionReloading, isTabReloading] = await Promise.all([
|
||||
DevStore.get('isExtensionReloading'),
|
||||
DevStore.get('isTabReloading'),
|
||||
]);
|
||||
|
||||
if (!isExtensionReloading) return sendResponse();
|
||||
|
||||
if (isTabReloading) {
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const tabToReload = tabs[0];
|
||||
|
||||
await DevStore.set('reloadTabId', tabToReload?.id);
|
||||
}
|
||||
chrome.runtime.reload();
|
||||
},
|
||||
};
|
||||
|
||||
export default hotReloadingHandler;
|
||||
@@ -1,6 +1,6 @@
|
||||
import TabManagementMessages from '@shared/messages/TabManagementMessages';
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
import openNewTab from '../util/openNewTab';
|
||||
import openNewTab from '@pages/background/util/openNewTab';
|
||||
import type TabManagementMessages from '@shared/messages/TabManagementMessages';
|
||||
import type { MessageHandler } from 'chrome-extension-toolkit';
|
||||
|
||||
const tabManagementHandler: MessageHandler<TabManagementMessages> = {
|
||||
getTabId({ sendResponse, sender }) {
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { UserScheduleMessages } from '@shared/messages/UserScheduleMessages';
|
||||
import addCourse from '@pages/background/lib/addCourse';
|
||||
import clearCourses from '@pages/background/lib/clearCourses';
|
||||
import createSchedule from '@pages/background/lib/createSchedule';
|
||||
import deleteSchedule from '@pages/background/lib/deleteSchedule';
|
||||
import removeCourse from '@pages/background/lib/removeCourse';
|
||||
import renameSchedule from '@pages/background/lib/renameSchedule';
|
||||
import switchSchedule from '@pages/background/lib/switchSchedule';
|
||||
import type { UserScheduleMessages } from '@shared/messages/UserScheduleMessages';
|
||||
import { Course } from '@shared/types/Course';
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
import addCourse from '../lib/addCourse';
|
||||
import clearCourses from '../lib/clearCourses';
|
||||
import createSchedule from '../lib/createSchedule';
|
||||
import deleteSchedule from '../lib/deleteSchedule';
|
||||
import removeCourse from '../lib/removeCourse';
|
||||
import renameSchedule from '../lib/renameSchedule';
|
||||
import switchSchedule from '../lib/switchSchedule';
|
||||
import type { MessageHandler } from 'chrome-extension-toolkit';
|
||||
|
||||
const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
|
||||
addCourse({ data, sendResponse }) {
|
||||
addCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
|
||||
addCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
|
||||
},
|
||||
removeCourse({ data, sendResponse }) {
|
||||
removeCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
|
||||
removeCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
|
||||
},
|
||||
clearCourses({ data, sendResponse }) {
|
||||
clearCourses(data.scheduleName).then(sendResponse);
|
||||
clearCourses(data.scheduleId).then(sendResponse);
|
||||
},
|
||||
switchSchedule({ data, sendResponse }) {
|
||||
switchSchedule(data.scheduleName).then(sendResponse);
|
||||
switchSchedule(data.scheduleId).then(sendResponse);
|
||||
},
|
||||
createSchedule({ data, sendResponse }) {
|
||||
createSchedule(data.scheduleName).then(sendResponse);
|
||||
},
|
||||
deleteSchedule({ data, sendResponse }) {
|
||||
deleteSchedule(data.scheduleName).then(sendResponse);
|
||||
deleteSchedule(data.scheduleId).then(sendResponse);
|
||||
},
|
||||
renameSchedule({ data, sendResponse }) {
|
||||
renameSchedule(data.scheduleName, data.newName).then(sendResponse);
|
||||
renameSchedule(data.scheduleId, data.newName).then(sendResponse);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
import { Course } from '@shared/types/Course';
|
||||
import type { Course } from '@shared/types/Course';
|
||||
import { getUnusedColor } from '@shared/util/colors';
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds a course to a user's schedule.
|
||||
* @param scheduleId - The id of the schedule to add the course to.
|
||||
* @param course - The course to add.
|
||||
* @returns A promise that resolves to void.
|
||||
* @throws An error if the schedule is not found.
|
||||
*/
|
||||
export default async function addCourse(scheduleName: string, course: Course): Promise<void> {
|
||||
export default async function addCourse(scheduleId: string, course: Course): Promise<void> {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
const activeSchedule = schedules.find(s => s.name === scheduleName);
|
||||
const activeSchedule = schedules.find(s => s.id === scheduleId);
|
||||
if (!activeSchedule) {
|
||||
throw new Error('Schedule not found');
|
||||
}
|
||||
|
||||
course.colors = getUnusedColor(activeSchedule, course);
|
||||
activeSchedule.courses.push(course);
|
||||
activeSchedule.updatedAt = Date.now();
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
|
||||
export default async function clearCourses(scheduleName: string): Promise<void> {
|
||||
/**
|
||||
* Clears the courses for a given schedule.
|
||||
* @param scheduleId - The id of the schedule.
|
||||
* @throws Error if the schedule does not exist.
|
||||
*/
|
||||
export default async function clearCourses(scheduleId: string): Promise<void> {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
const schedule = schedules.find(schedule => schedule.name === scheduleName);
|
||||
const schedule = schedules.find(schedule => schedule.id === scheduleId);
|
||||
if (!schedule) {
|
||||
throw new Error(`Schedule ${scheduleName} does not exist`);
|
||||
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||
}
|
||||
schedule.courses = [];
|
||||
schedule.updatedAt = Date.now();
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
import { generateRandomId } from '@shared/util/random';
|
||||
|
||||
/**
|
||||
* Creates a new schedule with the given name
|
||||
@@ -7,14 +8,16 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
*/
|
||||
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
if (schedules.find(schedule => schedule.name === scheduleName)) {
|
||||
return `Schedule ${scheduleName} already exists`;
|
||||
}
|
||||
// if (schedules.find(schedule => schedule.name === scheduleName)) {
|
||||
// return `Schedule ${scheduleName} already exists`;
|
||||
// }
|
||||
|
||||
schedules.push({
|
||||
id: generateRandomId(),
|
||||
name: scheduleName,
|
||||
courses: [],
|
||||
hours: 0
|
||||
hours: 0,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
|
||||
export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> {
|
||||
/**
|
||||
* Deletes a schedule with the specified name.
|
||||
*
|
||||
* @param scheduleId - The id of the schedule to delete.
|
||||
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is deleted successfully.
|
||||
*/
|
||||
export default async function deleteSchedule(scheduleId: string): Promise<string | undefined> {
|
||||
const [schedules, activeIndex] = await Promise.all([
|
||||
UserScheduleStore.get('schedules'),
|
||||
UserScheduleStore.get('activeIndex'),
|
||||
]);
|
||||
|
||||
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
|
||||
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||
if (scheduleIndex === -1) {
|
||||
return `Schedule ${scheduleName} does not exist`;
|
||||
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||
}
|
||||
if (scheduleIndex === activeIndex) {
|
||||
return 'Cannot delete active schedule';
|
||||
throw new Error('Cannot delete active schedule');
|
||||
}
|
||||
|
||||
schedules.splice(scheduleIndex, 1);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
import { Course } from '@shared/types/Course';
|
||||
import type { Course } from '@shared/types/Course';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default async function removeCourse(scheduleName: string, course: Course): Promise<void> {
|
||||
export default async function removeCourse(scheduleId: string, course: Course): Promise<void> {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
const activeSchedule = schedules.find(s => s.name === scheduleName);
|
||||
const activeSchedule = schedules.find(s => s.id === scheduleId);
|
||||
if (!activeSchedule) {
|
||||
throw new Error('Schedule not found');
|
||||
}
|
||||
|
||||
activeSchedule.courses = activeSchedule.courses.filter(c => c.uniqueId !== course.uniqueId);
|
||||
activeSchedule.updatedAt = Date.now();
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
|
||||
export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> {
|
||||
/**
|
||||
* Renames a schedule with the specified name to a new name.
|
||||
* @param scheduleId - The id of the schedule to be renamed.
|
||||
* @param newName - The new name for the schedule.
|
||||
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is renamed successfully.
|
||||
*/
|
||||
export default async function renameSchedule(scheduleId: string, newName: string): Promise<string | undefined> {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
|
||||
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||
if (scheduleIndex === -1) {
|
||||
return `Schedule ${scheduleName} does not exist`;
|
||||
}
|
||||
if (schedules.find(schedule => schedule.name === newName)) {
|
||||
return `Schedule ${newName} already exists`;
|
||||
return `Schedule ${scheduleId} does not exist`;
|
||||
}
|
||||
|
||||
schedules[scheduleIndex].name = newName;
|
||||
schedules[scheduleIndex]!.name = newName;
|
||||
// schedules[scheduleIndex].updatedAt = Date.now();
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
return undefined;
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
|
||||
export default async function switchSchedule(scheduleName: string): Promise<void> {
|
||||
/**
|
||||
* Switches the active schedule to the specified schedule name.
|
||||
* Throws an error if the schedule does not exist.
|
||||
* @param scheduleId - The id of the schedule to switch to.
|
||||
* @returns A Promise that resolves when the active schedule is successfully switched.
|
||||
*/
|
||||
export default async function switchSchedule(scheduleId: string): Promise<void> {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
|
||||
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
|
||||
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||
if (scheduleIndex === -1) {
|
||||
throw new Error(`Schedule ${scheduleName} does not exist`);
|
||||
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||
}
|
||||
|
||||
schedules[scheduleIndex]!.updatedAt = Date.now();
|
||||
|
||||
await UserScheduleStore.set('activeIndex', scheduleIndex);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { DevStore } from '@shared/storage/DevStore';
|
||||
|
||||
/**
|
||||
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
|
||||
*/
|
||||
const HOT_RELOADING_WHITELIST = [
|
||||
'youtube.com',
|
||||
'twitch.tv',
|
||||
'github.dev',
|
||||
'figma.com',
|
||||
'netflix.com',
|
||||
'disneyplus.com',
|
||||
'hbomax.com',
|
||||
'spotify.com',
|
||||
'localhost:6006',
|
||||
'docs.google.com',
|
||||
'reddit.com',
|
||||
'gmail.com',
|
||||
'photopea.com',
|
||||
'chat.openai.com',
|
||||
];
|
||||
|
||||
/**
|
||||
* Reloads the tab that was open when the extension was reloaded
|
||||
* @returns a promise that resolves when the tab is reloaded
|
||||
*/
|
||||
export async function hotReloadTab(): Promise<void> {
|
||||
const [isTabReloading, reloadTabId] = await Promise.all([
|
||||
DevStore.get('isTabReloading'),
|
||||
DevStore.get('reloadTabId'),
|
||||
]);
|
||||
|
||||
if (!isTabReloading || !reloadTabId) return;
|
||||
|
||||
chrome.tabs.get(reloadTabId, tab => {
|
||||
if (!tab?.id) return;
|
||||
if (!tab.url) return;
|
||||
if (!HOT_RELOADING_WHITELIST.find(url => tab.url?.includes(url))) {
|
||||
chrome.tabs.reload(tab.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export async function openDebugTab() {
|
||||
DevStore.get('wasDebugTabVisible'),
|
||||
]);
|
||||
|
||||
const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
|
||||
const isAlreadyOpen = (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
|
||||
if (isAlreadyOpen) return;
|
||||
|
||||
const tab = await chrome.tabs.create({
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export type TabWithId = Omit<chrome.tabs.Tab, 'id'> & { id: number };
|
||||
|
||||
/**
|
||||
* This is a helper function that opens a new tab in the current window, and focuses the window
|
||||
* @param tabIndex - the index of the tab to open the new tab at (optional)
|
||||
* @returns the tab that was opened
|
||||
*/
|
||||
export default async function openNewTab(url: string, tabIndex?: number): Promise<chrome.tabs.Tab> {
|
||||
const tab = await chrome.tabs.create({ url, index: tabIndex, active: true });
|
||||
export default async function openNewTab(url: string, tabIndex?: number): Promise<TabWithId> {
|
||||
const tab = (await chrome.tabs.create({ url, index: tabIndex, active: true })) as TabWithId;
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
return tab;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import React from 'react';
|
||||
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
|
||||
import Calendar from '@views/components/calendar/Calendar';
|
||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
|
||||
import { MessageListener } from 'chrome-extension-toolkit';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Calendar page
|
||||
* @returns entire page
|
||||
*/
|
||||
export default function CalendarMain() {
|
||||
useEffect(() => {
|
||||
const tabInfoListener = new MessageListener<TabInfoMessages>({
|
||||
getTabInfo: ({ sendResponse }) => {
|
||||
sendResponse({
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
tabInfoListener.listen();
|
||||
|
||||
return () => tabInfoListener.unlisten();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExtensionRoot>
|
||||
<ExtensionRoot className='h-full w-full'>
|
||||
<Calendar />
|
||||
</ExtensionRoot>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Calendar</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Calendar</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
<body style="min-height: 100vh; height: 0; margin: 0">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" style="height: 100%"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'uno.css';
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import CalendarMain from './CalendarMain';
|
||||
|
||||
createRoot(document.getElementById('root')).render(<CalendarMain />);
|
||||
createRoot(document.getElementById('root')!).render(<CalendarMain />);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import CourseCatalogMain from '@views/components/CourseCatalogMain';
|
||||
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
const support = getSiteSupport(window.location.href);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Debug</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Debug</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||
import Link from '@views/components/common/Link';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -7,7 +8,28 @@ import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'
|
||||
export default function App() {
|
||||
return (
|
||||
<ExtensionRoot>
|
||||
<div>hello how are you doing today.</div>
|
||||
<div className='text-base'>
|
||||
<div className='font-serif'>
|
||||
<i>“Real powerusers modify the sourcecode instead of using settings”</i> - doprz
|
||||
</div>
|
||||
<div className='font-serif'>
|
||||
<i>
|
||||
“become hackerman, go to{' '}
|
||||
<Link
|
||||
href='https://github.com/Longhorn-Developers/UT-Registration-Plus'
|
||||
className='link font-serif! italic!'
|
||||
>
|
||||
github
|
||||
</Link>{' '}
|
||||
yay”
|
||||
</i>{' '}
|
||||
- razboy20
|
||||
</div>
|
||||
<p className='mt-2.5 text-sm text-ut-gray'>
|
||||
{import.meta.env.VITE_PACKAGE_VERSION} - {import.meta.env.MODE}{' '}
|
||||
{import.meta.env.VITE_BETA_BUILD ? 'beta' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</ExtensionRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>UTRP Options</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
|
||||
createRoot(document.getElementById('root')).render(<App />);
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Popup</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 360px;
|
||||
height: 540px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'uno.css';
|
||||
|
||||
import PopupMain from '@views/components/PopupMain';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import PopupMain from '../../views/components/PopupMain';
|
||||
|
||||
createRoot(document.getElementById('root')).render(<PopupMain />);
|
||||
createRoot(document.getElementById('root')!).render(<PopupMain />);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
export default interface BrowserActionMessages {
|
||||
/** make it so that clicking the browser action will open the popup.html */
|
||||
enableBrowserAction: () => void;
|
||||
|
||||
10
src/shared/messages/CESMessage.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
interface CESMessage {
|
||||
/**
|
||||
* Opens the CES page for the specified instructor
|
||||
*
|
||||
* @param data first and last name of the instructor
|
||||
*/
|
||||
openCESPage: (data: { instructorFirstName: string; instructorLastName: string }) => chrome.tabs.Tab;
|
||||
}
|
||||
|
||||
export default CESMessage;
|
||||
19
src/shared/messages/CalendarMessages.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
interface CalendarBackgroundMessages {
|
||||
/**
|
||||
* Opens the calendar page if it is not already open, focuses the tab, and optionally opens the calendar for a specific course
|
||||
*
|
||||
* @param data - The unique id of the course to open the calendar page for (optional)
|
||||
*/
|
||||
switchToCalendarTab: (data: { uniqueId?: number }) => chrome.tabs.Tab;
|
||||
}
|
||||
|
||||
interface CalendarTabMessages {
|
||||
/**
|
||||
* Opens a popup for a specific course on the calendar page
|
||||
*
|
||||
* @param data - The unique id of the course to open on the calendar page
|
||||
*/
|
||||
openCoursePopup: (data: { uniqueId: number }) => chrome.tabs.Tab;
|
||||
}
|
||||
|
||||
export type { CalendarBackgroundMessages, CalendarTabMessages };
|
||||
@@ -1,3 +0,0 @@
|
||||
export default interface HotReloadingMessages {
|
||||
reloadExtension: () => void;
|
||||
}
|
||||
15
src/shared/messages/TabInfoMessages.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
type TabInfo = {
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
interface TabInfoMessages {
|
||||
/**
|
||||
* Gets the info for the tab receiving the message
|
||||
*
|
||||
* @returns The info for the tab receiving the message
|
||||
*/
|
||||
getTabInfo: () => TabInfo;
|
||||
}
|
||||
|
||||
export default TabInfoMessages;
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* This is a type with all the message definitions that can be sent TO specific tabs
|
||||
*/
|
||||
export default interface TAB_MESSAGES {}
|
||||
@@ -1,27 +1,30 @@
|
||||
import { Course } from '../types/Course';
|
||||
import type { Course } from '@shared/types/Course';
|
||||
|
||||
/**
|
||||
* Represents a collection of user schedule messages.
|
||||
*/
|
||||
export interface UserScheduleMessages {
|
||||
/**
|
||||
* Add a course to a schedule
|
||||
* @param data the schedule name and course to add
|
||||
* @param data the schedule id and course to add
|
||||
*/
|
||||
addCourse: (data: { scheduleName: string; course: Course }) => void;
|
||||
addCourse: (data: { scheduleId: string; course: Course }) => void;
|
||||
/**
|
||||
* Remove a course from a schedule
|
||||
* @param data the schedule name and course to remove
|
||||
* @param data the schedule id and course to remove
|
||||
*/
|
||||
removeCourse: (data: { scheduleName: string; course: Course }) => void;
|
||||
removeCourse: (data: { scheduleId: string; course: Course }) => void;
|
||||
/**
|
||||
* Clears all courses from a schedule
|
||||
* @param data the name of the schedule to clear
|
||||
* @param data the id of the schedule to clear
|
||||
*/
|
||||
clearCourses: (data: { scheduleName: string }) => void;
|
||||
clearCourses: (data: { scheduleId: string }) => void;
|
||||
|
||||
/**
|
||||
* Switches the active schedule to the one specified
|
||||
* @param data the name of the schedule to switch to
|
||||
* @param data the id of the schedule to switch to
|
||||
*/
|
||||
switchSchedule: (data: { scheduleName: string }) => void;
|
||||
switchSchedule: (data: { scheduleId: string }) => void;
|
||||
|
||||
/**
|
||||
* Creates a new schedule with the specified name
|
||||
@@ -31,14 +34,14 @@ export interface UserScheduleMessages {
|
||||
createSchedule: (data: { scheduleName: string }) => string | undefined;
|
||||
/**
|
||||
* Deletes a schedule with the specified name
|
||||
* @param data the name of the schedule to delete
|
||||
* @param data the id of the schedule to delete
|
||||
* @returns undefined if successful, otherwise an error message
|
||||
*/
|
||||
deleteSchedule: (data: { scheduleName: string }) => string | undefined;
|
||||
deleteSchedule: (data: { scheduleId: string }) => string | undefined;
|
||||
/**
|
||||
* Renames a schedule with the specified name
|
||||
* @param data the name of the schedule to rename and the new name
|
||||
* @param data the id of the schedule to rename and the new name
|
||||
* @returns undefined if successful, otherwise an error message
|
||||
*/
|
||||
renameSchedule: (data: { scheduleName: string; newName: string }) => string | undefined;
|
||||
renameSchedule: (data: { scheduleId: string; newName: string }) => string | undefined;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { createMessenger } from 'chrome-extension-toolkit';
|
||||
import BrowserActionMessages from './BrowserActionMessages';
|
||||
import TabManagementMessages from './TabManagementMessages';
|
||||
import TAB_MESSAGES from './TabMessages';
|
||||
import { UserScheduleMessages } from './UserScheduleMessages';
|
||||
|
||||
import type BrowserActionMessages from './BrowserActionMessages';
|
||||
import type { CalendarBackgroundMessages, CalendarTabMessages } from './CalendarMessages';
|
||||
import type CESMessage from './CESMessage';
|
||||
import type TabInfoMessages from './TabInfoMessages';
|
||||
import type TabManagementMessages from './TabManagementMessages';
|
||||
import type { UserScheduleMessages } from './UserScheduleMessages';
|
||||
|
||||
/**
|
||||
* This is a type with all the message definitions that can be sent TO the background script
|
||||
*/
|
||||
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & UserScheduleMessages;
|
||||
export type BACKGROUND_MESSAGES = BrowserActionMessages &
|
||||
TabManagementMessages &
|
||||
UserScheduleMessages &
|
||||
CESMessage &
|
||||
CalendarBackgroundMessages;
|
||||
|
||||
/**
|
||||
* This is a type with all the message definitions that can be sent TO specific tabs
|
||||
*/
|
||||
export type TAB_MESSAGES = CalendarTabMessages & TabInfoMessages;
|
||||
|
||||
/**
|
||||
* A utility object that can be used to send type-safe messages to the background script
|
||||
@@ -17,4 +29,4 @@ export const background = createMessenger<BACKGROUND_MESSAGES>('background');
|
||||
/**
|
||||
* A utility object that can be used to send type-safe messages to specific tabs
|
||||
*/
|
||||
export const tabs = createMessenger<TAB_MESSAGES>('tab');
|
||||
export const tabs = createMessenger<TAB_MESSAGES>('foreground');
|
||||
|
||||
@@ -15,6 +15,4 @@ export const ExtensionStore = createLocalStore<IExtensionStore>({
|
||||
lastUpdate: Date.now(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
debugStore({ ExtensionStore });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
|
||||
|
||||
import { generateRandomId } from '../util/random';
|
||||
|
||||
interface IUserScheduleStore {
|
||||
schedules: UserSchedule[];
|
||||
activeIndex: number;
|
||||
@@ -13,8 +15,10 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
|
||||
schedules: [
|
||||
new UserSchedule({
|
||||
courses: [],
|
||||
id: generateRandomId(),
|
||||
name: 'Schedule 1',
|
||||
hours: 0,
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
],
|
||||
activeIndex: 0,
|
||||
|
||||
27
src/shared/types/Color.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Represents a hexadecimal color value.
|
||||
*/
|
||||
export type HexColor = `#${string}`;
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid hexadecimal color value.
|
||||
*
|
||||
* @param color - The color string to check.
|
||||
* @returns A boolean indicating if the color is a valid hexadecimal color value.
|
||||
*/
|
||||
export const isHexColor = (color: string): color is HexColor => /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
|
||||
|
||||
/**
|
||||
* Represents an RGB color value.
|
||||
*/
|
||||
export type RGB = [r: number, g: number, b: number];
|
||||
|
||||
/**
|
||||
* Represents a linear sRGB color value.
|
||||
*/
|
||||
export type sRGB = [r: number, g: number, b: number];
|
||||
|
||||
/**
|
||||
* Represents a Lab color value.
|
||||
*/
|
||||
export type Lab = [l: number, a: number, b: number];
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Serialized } from 'chrome-extension-toolkit';
|
||||
import { CourseMeeting } from './CourseMeeting';
|
||||
import { getCourseColors } from '@shared/util/colors';
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
|
||||
import type { CourseMeeting } from './CourseMeeting';
|
||||
import { CourseSchedule } from './CourseSchedule';
|
||||
import Instructor from './Instructor';
|
||||
import type { CourseColors } from './ThemeColors';
|
||||
|
||||
/**
|
||||
* Whether the class is taught online, in person, or a hybrid of the two
|
||||
@@ -12,12 +14,17 @@ export type InstructionMode = 'Online' | 'In Person' | 'Hybrid';
|
||||
/**
|
||||
* The status of a course (e.g. open, closed, waitlisted, cancelled)
|
||||
*/
|
||||
export enum Status {
|
||||
OPEN = 'OPEN',
|
||||
CLOSED = 'CLOSED',
|
||||
WAITLISTED = 'WAITLISTED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
}
|
||||
export const Status = {
|
||||
OPEN: 'OPEN',
|
||||
CLOSED: 'CLOSED',
|
||||
WAITLISTED: 'WAITLISTED',
|
||||
CANCELLED: 'CANCELLED',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Represents the type of status for a course.
|
||||
*/
|
||||
export type StatusType = (typeof Status)[keyof typeof Status];
|
||||
|
||||
/**
|
||||
* Represents a semester, with the year and the season for when a course is offered
|
||||
@@ -36,43 +43,51 @@ export type Semester = {
|
||||
*/
|
||||
export class Course {
|
||||
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
|
||||
uniqueId: number;
|
||||
uniqueId!: number;
|
||||
/** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */
|
||||
number: string;
|
||||
number!: string;
|
||||
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
|
||||
fullName: string;
|
||||
fullName!: string;
|
||||
/** Just the english name for a course, without the number and department */
|
||||
courseName: string;
|
||||
courseName!: string;
|
||||
/** The unique identifier for which department that a course belongs to, i.e. CS, MAL, etc. */
|
||||
department: string;
|
||||
department!: string;
|
||||
|
||||
/** The number of credits that a course is worth */
|
||||
creditHours: number;
|
||||
creditHours!: number;
|
||||
/** Is the course open, closed, waitlisted, or cancelled? */
|
||||
status: Status;
|
||||
status!: StatusType;
|
||||
/** all the people that are teaching this course, and some metadata about their names */
|
||||
instructors: Instructor[];
|
||||
/** Some courses at UT are reserved for certain groups of people or people within a certain major, which makes it difficult for people outside of that group to register for the course. */
|
||||
isReserved: boolean;
|
||||
isReserved!: boolean;
|
||||
/** The description of the course as an array of "lines". This will include important information as well as a short summary of the topics covered */
|
||||
description?: string[];
|
||||
/** The schedule for the course, which includes the days of the week that the course is taught, the time that the course is taught, and the location that the course is taught */
|
||||
schedule: CourseSchedule;
|
||||
/** the link to the course details page for this course */
|
||||
url: string;
|
||||
url!: string;
|
||||
/** the link to the registration page for this course, for easy access when registering */
|
||||
registerURL?: string;
|
||||
/** At UT, some courses have certain "flags" which aid in graduation */
|
||||
flags: string[];
|
||||
flags!: string[];
|
||||
/** How is the class being taught (online, hybrid, in person, etc) */
|
||||
instructionMode: InstructionMode;
|
||||
instructionMode!: InstructionMode;
|
||||
/** Which semester is the course from */
|
||||
semester: Semester;
|
||||
semester!: Semester;
|
||||
/** Unix timestamp of when the course was last scraped */
|
||||
scrapedAt!: number;
|
||||
/** The colors of the course when displayed */
|
||||
colors: CourseColors;
|
||||
|
||||
constructor(course: Serialized<Course>) {
|
||||
Object.assign(this, course);
|
||||
this.schedule = new CourseSchedule(course.schedule);
|
||||
this.instructors = course.instructors.map(i => new Instructor(i));
|
||||
if (!course.scrapedAt) {
|
||||
this.scrapedAt = Date.now();
|
||||
}
|
||||
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Serialized } from 'chrome-extension-toolkit';
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
|
||||
/**
|
||||
* a map of the days of the week that a class is taught, and the corresponding abbreviation
|
||||
@@ -30,11 +30,15 @@ export type Location = {
|
||||
*/
|
||||
export class CourseMeeting {
|
||||
/** The day of the week that the course is taught */
|
||||
days: Day[];
|
||||
/** The start time of the course, in minutes since midnight */
|
||||
startTime: number;
|
||||
/** The end time of the course, in minutes since midnight */
|
||||
endTime: number;
|
||||
days!: Day[];
|
||||
/** NOTE: Times starting and after 12 PM have an additional 720 minutes (12 hrs) added to them
|
||||
* The start time of the course, in minutes since midnight
|
||||
* */
|
||||
startTime!: number;
|
||||
/** NOTE: Times starting and after 12 PM have an additional 720 minutes (12 hrs) added to them
|
||||
* The end time of the course, in minutes since midnight
|
||||
* */
|
||||
endTime!: number;
|
||||
/** The location that the course is taught */
|
||||
location?: Location;
|
||||
|
||||
@@ -104,7 +108,7 @@ export class CourseMeeting {
|
||||
}
|
||||
|
||||
startTimeString += startMinute === 0 ? ':00' : `:${startMinute}`;
|
||||
startTimeString += startHour >= 12 ? 'pm' : 'am';
|
||||
startTimeString += startHour >= 12 ? ' p.m.' : ' a.m.';
|
||||
|
||||
if (endHour === 0) {
|
||||
endTimeString = '12';
|
||||
@@ -114,7 +118,7 @@ export class CourseMeeting {
|
||||
endTimeString = `${endHour}`;
|
||||
}
|
||||
endTimeString += endMinute === 0 ? ':00' : `:${endMinute}`;
|
||||
endTimeString += endHour >= 12 ? 'pm' : 'am';
|
||||
endTimeString += endHour >= 12 ? ' p.m.' : ' a.m.';
|
||||
|
||||
if (options.capitalize) {
|
||||
startTimeString = startTimeString.toUpperCase();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Serialized } from 'chrome-extension-toolkit';
|
||||
import { CourseMeeting, Day, DAY_MAP } from './CourseMeeting';
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
|
||||
import type { Day } from './CourseMeeting';
|
||||
import { CourseMeeting, DAY_MAP } from './CourseMeeting';
|
||||
|
||||
/**
|
||||
* This represents the schedule for a course, which includes all the meeting times for the course, as well as helper functions for parsing, serializing, and deserializing the schedule
|
||||
@@ -8,10 +10,12 @@ export class CourseSchedule {
|
||||
meetings: CourseMeeting[] = [];
|
||||
|
||||
constructor(courseSchedule?: Serialized<CourseSchedule>) {
|
||||
if (!courseSchedule) {
|
||||
if (!courseSchedule || courseSchedule.meetings === undefined) {
|
||||
this.meetings = [];
|
||||
return;
|
||||
}
|
||||
Object.assign(this, courseSchedule);
|
||||
|
||||
this.meetings = courseSchedule.meetings.map(meeting => new CourseMeeting(meeting));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +38,7 @@ export class CourseSchedule {
|
||||
if (char === 'S' && nextChar === 'U') {
|
||||
day += nextChar;
|
||||
}
|
||||
return DAY_MAP[day];
|
||||
return DAY_MAP[day as keyof typeof DAY_MAP];
|
||||
})
|
||||
.filter(Boolean) as Day[];
|
||||
|
||||
@@ -42,28 +46,37 @@ export class CourseSchedule {
|
||||
.replaceAll('.', '')
|
||||
.split('-')
|
||||
.map(time => {
|
||||
const [hour, rest] = time.split(':');
|
||||
const [minute, ampm] = rest.split(' ');
|
||||
const [rawHour, rest] = time.split(':');
|
||||
const [rawMinute, ampm] = rest?.split(' ') ?? ['', ''];
|
||||
const hour = (rawHour === '12' ? 0 : Number(rawHour)) + (ampm === 'pm' ? 12 : 0);
|
||||
const minute = Number(rawMinute);
|
||||
|
||||
if (ampm === 'pm') {
|
||||
return Number(hour) * 60 + Number(minute) + 12 * 60;
|
||||
}
|
||||
return Number(hour) * 60 + Number(minute);
|
||||
return hour * 60 + minute;
|
||||
});
|
||||
|
||||
const location = locLine.split(' ').filter(Boolean);
|
||||
|
||||
if (startTime === undefined || endTime === undefined) {
|
||||
throw new Error('Failed to parse time');
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
throw new Error('Start time must be before end time');
|
||||
}
|
||||
|
||||
if (location === undefined) {
|
||||
throw new Error('Failed to parse location');
|
||||
}
|
||||
|
||||
return new CourseMeeting({
|
||||
days,
|
||||
startTime,
|
||||
endTime,
|
||||
location: location.length
|
||||
? {
|
||||
building: location[0],
|
||||
room: location[1],
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
location: {
|
||||
building: location[0] ?? '',
|
||||
room: location[1] ?? '',
|
||||
},
|
||||
} satisfies Serialized<CourseMeeting>);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse schedule: ${dayLine} ${timeLine} ${locLine}`);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { PointOptionsObject } from 'highcharts';
|
||||
import { Semester } from './Course';
|
||||
/**
|
||||
* Each of the possible letter grades that can be given in a course
|
||||
*/
|
||||
export type LetterGrade = 'A' | 'A-' | 'B' | 'B+' | 'B-' | 'C' | 'C+' | 'C-' | 'D' | 'D+' | 'D-' | 'F';
|
||||
export type LetterGrade = 'A' | 'A-' | 'B' | 'B+' | 'B-' | 'C' | 'C+' | 'C-' | 'D' | 'D+' | 'D-' | 'F' | 'Other';
|
||||
|
||||
/**
|
||||
* A distribution of grades for a course,
|
||||
@@ -18,23 +15,26 @@ export type Distribution = {
|
||||
* This is a object-ified version of a row in the SQL table that is used to store the distribution data.
|
||||
*/
|
||||
export type CourseSQLRow = {
|
||||
sem?: string;
|
||||
prof?: string;
|
||||
dept?: string;
|
||||
course_nbr?: string;
|
||||
course_name?: string;
|
||||
a1?: number;
|
||||
a2?: number;
|
||||
a3?: number;
|
||||
b1?: number;
|
||||
b2?: number;
|
||||
b3?: number;
|
||||
c1?: number;
|
||||
c2?: number;
|
||||
c3?: number;
|
||||
d1?: number;
|
||||
d2?: number;
|
||||
d3?: number;
|
||||
f?: number;
|
||||
semesters?: string;
|
||||
Semester: string;
|
||||
Section: number;
|
||||
Department: string;
|
||||
Department_Code: string;
|
||||
Course_Number: string;
|
||||
Course_Title: string;
|
||||
Course_Full_Title: string;
|
||||
Instructor_First: string | null;
|
||||
Instructor_Last: string | null;
|
||||
A: number;
|
||||
A_Minus: number;
|
||||
B_Plus: number;
|
||||
B: number;
|
||||
B_Minus: number;
|
||||
C_Plus: number;
|
||||
C: number;
|
||||
C_Minus: number;
|
||||
D_Plus: number;
|
||||
D: number;
|
||||
D_Minus: number;
|
||||
F: number;
|
||||
Other: number;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Serialized } from 'chrome-extension-toolkit';
|
||||
import { capitalize } from '../util/string';
|
||||
import { capitalize } from '@shared/util/string';
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
|
||||
/**
|
||||
* A type representing an instructor for a course (who teaches it)
|
||||
*/
|
||||
export default class Instructor {
|
||||
fullName: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
middleInitial?: string;
|
||||
|
||||
constructor(instructor: Serialized<Instructor>) {
|
||||
@@ -53,16 +53,16 @@ export default class Instructor {
|
||||
return capitalize(str);
|
||||
};
|
||||
|
||||
if (format === 'abbr') {
|
||||
if (format === 'abbr' && firstName && lastName && firstName[0]) {
|
||||
return `${process(firstName[0])}. ${process(lastName)}`;
|
||||
}
|
||||
if (format === 'full_name') {
|
||||
if (format === 'full_name' && fullName) {
|
||||
return process(fullName);
|
||||
}
|
||||
if (format === 'first_last') {
|
||||
if (format === 'first_last' && firstName && lastName) {
|
||||
return `${process(firstName)} ${process(lastName)}`;
|
||||
}
|
||||
if (format === 'last') {
|
||||
if (format === 'last' && lastName) {
|
||||
return process(lastName);
|
||||
}
|
||||
|
||||
|
||||
17
src/shared/types/MIMEType.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const MIMEType = {
|
||||
JSON: 'application/json',
|
||||
HTML: 'text/html',
|
||||
TEXT: 'text/plain',
|
||||
FORM: 'application/x-www-form-urlencoded',
|
||||
MULTIPART: 'multipart/form-data',
|
||||
PDF: 'application/pdf',
|
||||
IMAGE: 'image/*',
|
||||
AUDIO: 'audio/*',
|
||||
VIDEO: 'video/*',
|
||||
CALENDAR: 'text/calendar',
|
||||
ANY: '*/*',
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export type MIMETypeKey = keyof typeof MIMEType;
|
||||
|
||||
export default MIMEType;
|
||||
80
src/shared/types/ThemeColors.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { theme } from 'unocss/preset-mini';
|
||||
|
||||
import type { HexColor } from './Color';
|
||||
|
||||
export const colors = {
|
||||
ut: {
|
||||
burntorange: '#BF5700',
|
||||
black: '#333F48',
|
||||
orange: '#F8971F',
|
||||
yellow: '#FFD600',
|
||||
lightgreen: '#A6CD57',
|
||||
green: '#579D42',
|
||||
teal: '#00A9B7',
|
||||
blue: '#005F86',
|
||||
gray: '#9CADB7',
|
||||
offwhite: '#D6D2C4',
|
||||
concrete: '#95A5A6',
|
||||
red: '#B91C1C', // Not sure if this should be here, but it's used for remove course, and add course is ut-green
|
||||
},
|
||||
theme: {
|
||||
red: '#BF0000',
|
||||
black: '#1A2024',
|
||||
},
|
||||
} as const satisfies Record<string, Record<string, string>>;
|
||||
|
||||
export const extendedColors = {
|
||||
...colors,
|
||||
gradeDistribution: {
|
||||
a: '#22C55E',
|
||||
aminus: '#A3E635',
|
||||
bplus: '#84CC16',
|
||||
b: '#FDE047',
|
||||
bminus: '#FACC15',
|
||||
cplus: '#F59E0B',
|
||||
c: '#FB923C',
|
||||
cminus: '#F97316',
|
||||
dplus: '#EF4444',
|
||||
d: '#DC2626',
|
||||
dminus: '#B91C1C',
|
||||
f: '#B91C1C',
|
||||
other: '#6B7280',
|
||||
},
|
||||
} as const;
|
||||
|
||||
type NestedKeys<T> = {
|
||||
[K in keyof T]: T[K] extends Record<string, unknown> ? `${string & K}-${string & keyof T[K]}` : never;
|
||||
}[keyof T];
|
||||
|
||||
/**
|
||||
* A union of all colors in the theme
|
||||
*/
|
||||
export type ThemeColor = NestedKeys<typeof colors>;
|
||||
|
||||
/**
|
||||
* Represents a Tailwind colorway: a colorway is a key in the theme.colors object that has an object as its value.
|
||||
*/
|
||||
export type TWColorway = {
|
||||
[K in keyof typeof theme.colors]: (typeof theme.colors)[K] extends Record<string, unknown> ? K : never;
|
||||
}[keyof typeof theme.colors];
|
||||
export type TWIndex = keyof (typeof theme.colors)[TWColorway];
|
||||
|
||||
/**
|
||||
* Represents the colors for a course.
|
||||
*/
|
||||
export interface CourseColors {
|
||||
primaryColor: HexColor;
|
||||
secondaryColor: HexColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusted colorway indexes for better *quality*
|
||||
*/
|
||||
export const colorwayIndexes = {
|
||||
yellow: 300,
|
||||
amber: 400,
|
||||
emerald: 400,
|
||||
lime: 400,
|
||||
orange: 400,
|
||||
sky: 600,
|
||||
} as const satisfies Record<string, number>;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Serialized } from 'chrome-extension-toolkit';
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
|
||||
import { generateRandomId } from '../util/random';
|
||||
import { Course } from './Course';
|
||||
|
||||
/**
|
||||
@@ -6,19 +8,21 @@ import { Course } from './Course';
|
||||
*/
|
||||
export class UserSchedule {
|
||||
courses: Course[];
|
||||
id: string;
|
||||
name: string;
|
||||
hours: number;
|
||||
/** Unix timestamp of when the schedule was last updated */
|
||||
updatedAt: number;
|
||||
|
||||
constructor(schedule: Serialized<UserSchedule>) {
|
||||
this.courses = schedule.courses.map(c => new Course(c));
|
||||
this.id = schedule.id ?? generateRandomId();
|
||||
this.name = schedule.name;
|
||||
this.hours = 0;
|
||||
for (const course of this.courses) {
|
||||
this.hours += course.creditHours;
|
||||
}
|
||||
this.hours = this.courses.reduce((acc, c) => acc + c.creditHours, 0);
|
||||
this.updatedAt = schedule.updatedAt ?? 0;
|
||||
}
|
||||
|
||||
containsCourse(course: Course): boolean {
|
||||
return this.courses.some(c => c.uniqueId === course.uniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,252 @@
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
import { theme } from 'unocss/preset-mini';
|
||||
|
||||
export interface CourseColors {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
import type { HexColor, Lab, RGB, sRGB } from '../types/Color';
|
||||
import { isHexColor } from '../types/Color';
|
||||
import type { Course } from '../types/Course';
|
||||
import type { CourseColors, TWColorway, TWIndex } from '../types/ThemeColors';
|
||||
import { colorwayIndexes } from '../types/ThemeColors';
|
||||
import type { UserSchedule } from '../types/UserSchedule';
|
||||
|
||||
/**
|
||||
* Converts a hexadecimal color value to RGB format. (adapted from https://stackoverflow.com/a/5624139/8022866)
|
||||
*
|
||||
* @param hex - The hexadecimal color value.
|
||||
* @returns An array containing the RGB values.
|
||||
*/
|
||||
export function hexToRGB(hex: HexColor): RGB | undefined {
|
||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
const parsedHex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
|
||||
|
||||
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(parsedHex);
|
||||
|
||||
if (!result || !(result.length > 3)) return undefined;
|
||||
|
||||
return [parseInt(result[1]!, 16), parseInt(result[2]!, 16), parseInt(result[3]!, 16)];
|
||||
}
|
||||
|
||||
// calculates luminance of a hex string
|
||||
export function getLuminance(hex: string): number {
|
||||
let r = parseInt(hex.substring(1, 3), 16);
|
||||
let g = parseInt(hex.substring(3, 5), 16);
|
||||
let b = parseInt(hex.substring(5, 7), 16);
|
||||
export const useableColorways = Object.keys(theme.colors)
|
||||
// check that the color is a colorway (is an object)
|
||||
.filter(color => typeof theme.colors[color as keyof typeof theme.colors] === 'object')
|
||||
.slice(0, 17) as TWColorway[];
|
||||
|
||||
[r, g, b] = [r, g, b].map(color => {
|
||||
let c = color / 255;
|
||||
/**
|
||||
* Generate a Tailwind classname for the font color based on the background color
|
||||
* @param bgColor the hex color of the background
|
||||
*/
|
||||
export function pickFontColor(bgColor: HexColor): 'text-white' | 'text-black' | 'text-theme-black' {
|
||||
const coefficients = [0.2126729, 0.7151522, 0.072175] as const;
|
||||
|
||||
c = c > 0.03928 ? ((c + 0.055) / 1.055) ** 2.4 : (c /= 12.92);
|
||||
const flipYs = 0.342; // based on APCA™ 0.98G middle contrast BG color
|
||||
|
||||
return c;
|
||||
});
|
||||
const trc = 2.4; // 2.4 exponent for emulating actual monitor perception
|
||||
const rgb = hexToRGB(bgColor);
|
||||
if (!rgb) throw new Error('bgColor: Invalid hex.');
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
// coefficients and rgb are both 3 elements long, so this is safe
|
||||
let Ys = rgb.reduce((acc, c, i) => acc + (c / 255.0) ** trc * coefficients[i]!, 0);
|
||||
|
||||
// calculates contrast ratio between two hex strings
|
||||
function contrastRatioPair(hex1: string, hex2: string) {
|
||||
const lum1 = getLuminance(hex1);
|
||||
const lum2 = getLuminance(hex2);
|
||||
if (Ys < flipYs) {
|
||||
return 'text-white';
|
||||
}
|
||||
|
||||
return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05);
|
||||
return Ys < 0.365 ? 'text-black' : 'text-theme-black';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a tailwind classname for the font color based on the background color
|
||||
* @param bgColor the tailwind classname for background ex. "bg-emerald-500"
|
||||
* Get primary and secondary colors from a Tailwind colorway
|
||||
* @param colorway the Tailwind colorway ex. "emerald"
|
||||
*/
|
||||
export function pickFontColor(bgColor: string): 'text-white' | 'text-black' {
|
||||
return contrastRatioPair(bgColor, '#606060') > contrastRatioPair(bgColor, '#ffffff') ? 'text-black' : 'text-white';
|
||||
}
|
||||
export function getCourseColors(colorway: TWColorway, index?: number, offset: number = 300): CourseColors {
|
||||
if (index === undefined) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
index = colorway in colorwayIndexes ? colorwayIndexes[colorway as keyof typeof colorwayIndexes] : 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary and secondary colors from a tailwind colorway
|
||||
* @param colorway the tailwind colorway ex. "emerald"
|
||||
*/
|
||||
export function getCourseColors(colorway: keyof typeof theme.colors, index = 600, offset = 200): CourseColors {
|
||||
return {
|
||||
primaryColor: theme.colors[colorway][index] as string,
|
||||
secondaryColor: theme.colors[colorway][index + offset] as string,
|
||||
primaryColor: theme.colors[colorway][index as TWIndex] as HexColor,
|
||||
secondaryColor: theme.colors[colorway][(index + offset) as TWIndex] as HexColor,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tailwind colorway from a given color.
|
||||
*
|
||||
* @param color - The hexadecimal color value.
|
||||
* @returns The Tailwind colorway.
|
||||
*/
|
||||
export function getColorwayFromColor(color: HexColor): TWColorway {
|
||||
for (const colorway of useableColorways) {
|
||||
if (Object.values(theme.colors[colorway]).includes(color)) {
|
||||
return colorway as TWColorway;
|
||||
}
|
||||
}
|
||||
|
||||
// not a direct match, get the closest color
|
||||
let closestColor = '';
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const colorway of useableColorways) {
|
||||
for (const [shade, shadeColor] of Object.entries(theme.colors[colorway])) {
|
||||
// type guard
|
||||
if (!isHexColor(shadeColor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const shadeColorRGB = hexToRGB(shadeColor);
|
||||
if (!shadeColorRGB) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = oklabDistance(rgbToOKlab(shadeColorRGB), rgbToOKlab(shadeColorRGB));
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestColor = shade;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// type guard
|
||||
if (!isHexColor(closestColor)) {
|
||||
throw new Error("closestColor isn't a valid hex color");
|
||||
}
|
||||
|
||||
return getColorwayFromColor(closestColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next unused color in a tailwind colorway for a given schedule
|
||||
* @param schedule the schedule which the course is in
|
||||
* @param course the course to get the color for
|
||||
*/
|
||||
export function getUnusedColor(
|
||||
schedule: Serialized<UserSchedule>,
|
||||
course: Course,
|
||||
index?: number,
|
||||
offset?: number
|
||||
): CourseColors {
|
||||
// strategy: First, check if any of the course's in schedule have the same department as the current course,
|
||||
// if so, use a colorway near the color of that course if possible.
|
||||
// Otherwise, find a colorway that is not used in the schedule and at least two hues away from any other color in the schedule.
|
||||
|
||||
// wrapping helper functions
|
||||
function getPreviousColorway(colorway: TWColorway): TWColorway {
|
||||
const colorwayIndex = useableColorways.indexOf(colorway);
|
||||
return useableColorways[(colorwayIndex - 1 + useableColorways.length) % useableColorways.length] as TWColorway;
|
||||
}
|
||||
|
||||
function getNextColorway(colorway: TWColorway): TWColorway {
|
||||
const colorwayIndex = useableColorways.indexOf(colorway);
|
||||
return useableColorways[(colorwayIndex + 1) % useableColorways.length] as TWColorway;
|
||||
}
|
||||
|
||||
const scheduleCourses = schedule.courses.map(c => ({
|
||||
...c,
|
||||
colorway: getColorwayFromColor(c.colors.primaryColor),
|
||||
}));
|
||||
const usedColorways = new Set(scheduleCourses.map(c => c.colorway));
|
||||
const availableColorways = new Set(useableColorways.filter(c => !usedColorways.has(c)));
|
||||
|
||||
if (availableColorways.size > 0) {
|
||||
let sameDepartment = scheduleCourses.filter(c => c.department === course.department);
|
||||
|
||||
sameDepartment.sort((a, b) => {
|
||||
const aIndex = useableColorways.indexOf(a.colorway);
|
||||
const bIndex = useableColorways.indexOf(b.colorway);
|
||||
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
if (sameDepartment.length > 0) {
|
||||
// check to see if any adjacent colorways are available
|
||||
const centerCourse = sameDepartment[Math.floor(Math.random() * sameDepartment.length)]!;
|
||||
|
||||
let nextColorway = getNextColorway(centerCourse.colorway);
|
||||
let prevColorway = getPreviousColorway(centerCourse.colorway);
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (availableColorways.has(nextColorway)) {
|
||||
return getCourseColors(nextColorway, index, offset);
|
||||
}
|
||||
if (availableColorways.has(prevColorway)) {
|
||||
return getCourseColors(prevColorway, index, offset);
|
||||
}
|
||||
|
||||
nextColorway = getNextColorway(nextColorway);
|
||||
prevColorway = getPreviousColorway(prevColorway);
|
||||
}
|
||||
}
|
||||
|
||||
const shortenedColorways = new Set(availableColorways);
|
||||
// no courses in the same department, restrict colorways to those which are at least 2 indexes away from any used colors
|
||||
for (const colorway of usedColorways) {
|
||||
shortenedColorways.delete(getPreviousColorway(colorway));
|
||||
shortenedColorways.delete(getNextColorway(colorway));
|
||||
}
|
||||
|
||||
if (shortenedColorways.size > 0) {
|
||||
// TODO: make this go by 3's to leave future spaces open
|
||||
const randomColorway = Array.from(shortenedColorways)[Math.floor(Math.random() * shortenedColorways.size)]!;
|
||||
|
||||
return getCourseColors(randomColorway, index, offset);
|
||||
}
|
||||
// no colorways are at least 2 indexes away from any used colors, just get a random colorway
|
||||
const randomColorway: TWColorway | undefined =
|
||||
Array.from(availableColorways)[Math.floor(Math.random() * availableColorways.size)];
|
||||
|
||||
if (!randomColorway) {
|
||||
throw new Error('randomColorway is undefined');
|
||||
}
|
||||
|
||||
return getCourseColors(randomColorway, index, offset);
|
||||
}
|
||||
// TODO: get just a random color idk
|
||||
return getCourseColors('emerald', index, offset);
|
||||
}
|
||||
|
||||
// OKLab helper functions (https://github.com/bottosson/bottosson.github.io/blob/master/misc/colorpicker/colorconversion.js)
|
||||
function srgbTransferFunction(a: number): number {
|
||||
return a <= 0.0031308 ? 12.92 * a : 1.055 * a ** 0.4166666666666667 - 0.055;
|
||||
}
|
||||
|
||||
function srgbTransferFunctionInv(a: number): number {
|
||||
return a > 0.04045 ? ((a + 0.055) / 1.055) ** 2.4 : a / 12.92;
|
||||
}
|
||||
|
||||
function rgbToSrgb(rgb: RGB): sRGB {
|
||||
return rgb.map(c => srgbTransferFunctionInv(c / 255)) as sRGB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an RGB color to the OKLab color space
|
||||
* @param rgb the RGB color
|
||||
* @returns the color in the OKLab color space
|
||||
*/
|
||||
function srgbToOKlab([r, g, b]: sRGB): Lab {
|
||||
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
||||
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
||||
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
||||
|
||||
let lc = Math.cbrt(l);
|
||||
let mc = Math.cbrt(m);
|
||||
let sc = Math.cbrt(s);
|
||||
|
||||
return [
|
||||
0.2104542553 * lc + 0.793617785 * mc - 0.0040720468 * sc,
|
||||
1.9779984951 * lc - 2.428592205 * mc + 0.4505937099 * sc,
|
||||
0.0259040371 * lc + 0.7827717662 * mc - 0.808675766 * sc,
|
||||
];
|
||||
}
|
||||
|
||||
function rgbToOKlab(rgb: RGB): Lab {
|
||||
return srgbToOKlab(rgbToSrgb(rgb));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance between two colors in the OKLab color space
|
||||
*/
|
||||
function oklabDistance([l1, a1, b1]: Lab, [l2, a2, b2]: Lab): number {
|
||||
return Math.sqrt((l2 - l1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2);
|
||||
}
|
||||
|
||||