Compare commits
196 Commits
v2.0.2
...
injected-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4dd36f27 | ||
|
|
5daccc8349 | ||
|
|
e4ce051086 | ||
|
|
d5a04c745f | ||
|
|
f01cb070b3 | ||
| b2eac59ae7 | |||
| e99664fdae | |||
| 49c0d63f0b | |||
|
|
8c82282467 | ||
|
|
0a3c31ec09 | ||
|
|
ce7917b474 | ||
|
|
a5fe6ec06b | ||
|
|
5a2ee0d19a | ||
|
|
9ec0d106f5 | ||
|
|
70a3f14e0a | ||
|
|
d69707b8e8 | ||
|
|
41e6d77d02 | ||
|
|
11fece0595 | ||
|
|
c676be4765 | ||
|
|
b4c96a9a10 | ||
|
|
adba5e1bbc | ||
|
|
58dc706ece | ||
|
|
70c75ff481 | ||
|
|
7f76af7ab3 | ||
|
|
a538f20aad | ||
|
|
0acd0b722c | ||
| ae32d0b645 | |||
|
|
51b6799d73 | ||
|
|
f214ed7c01 | ||
|
|
141f5cc70e | ||
|
|
6883a0bc8d | ||
|
|
e44b0c0e45 | ||
|
|
206c97c5b5 | ||
| ac71b838db | |||
|
|
4f5753917b | ||
|
|
478ab31706 | ||
|
|
79c5c97d98 | ||
|
|
42420f5502 | ||
|
|
0ced198f01 | ||
|
|
ff4ee494b6 | ||
|
|
a03bcf17b8 | ||
|
|
314ea09b41 | ||
|
|
1891bd941e | ||
|
|
af991e6609 | ||
|
|
fe3521dec7 | ||
|
|
a363efb2d2 | ||
|
|
e7ba9c54a6 | ||
|
|
d69e3435cf | ||
|
|
b554b4344d | ||
|
|
14f241d603 | ||
|
|
419e3066f1 | ||
|
|
ed3ff846d8 | ||
|
|
7132bcf572 | ||
|
|
0bc30d3d1e | ||
|
|
3878c7104e | ||
|
|
851947db0b | ||
|
|
724e1a1d19 | ||
|
|
66fea21abd | ||
|
|
7550b55b9b | ||
|
|
65f0cb27af | ||
|
|
9ce69c2f2e | ||
|
|
a46526fa40 | ||
|
|
c5968f3f11 | ||
|
|
bba067f591 | ||
|
|
71d8ac7486 | ||
|
|
2997cb87d4 | ||
|
|
8b2d07033c | ||
|
|
b8fe5109a9 | ||
|
|
2cffb794db | ||
|
|
73fe14e17a | ||
|
|
33ca937d12 | ||
|
|
c0968ef7a0 | ||
|
|
3c7b35d5f3 | ||
|
|
8edd062588 | ||
|
|
42d24f6367 | ||
|
|
2f19c57553 | ||
|
|
e3b8e4c270 | ||
|
|
5aef43496f | ||
|
|
e1224e11af | ||
|
|
7f826c2a78 | ||
|
|
89a8e42059 | ||
|
|
ed8915bcd1 | ||
|
|
1168584b8a | ||
|
|
a0496fea18 | ||
|
|
fd747e9e8e | ||
|
|
27fdd9c559 | ||
|
|
ba22ba427c | ||
|
|
3e4cec43d1 | ||
|
|
633c9ba593 | ||
|
|
0f14b8ce1b | ||
|
|
bee506b79b | ||
|
|
d8a4c91a80 | ||
|
|
14ab537db1 | ||
|
|
939208532b | ||
|
|
8e3502593e | ||
|
|
7b7b858cf5 | ||
|
|
82b467a223 | ||
|
|
f049a25aac | ||
|
|
39b4396f88 | ||
|
|
854d24bf0d | ||
|
|
c47320e9a3 | ||
|
|
982b7de50e | ||
|
|
17efb94e68 | ||
|
|
40631e2421 | ||
|
|
81e02c8187 | ||
|
|
d907a43552 | ||
|
|
3e1a20a9f2 | ||
|
|
d21843468b | ||
|
|
6b7e45b5f4 | ||
|
|
36b9189a6c | ||
|
|
80148ba0ff | ||
|
|
776cab9297 | ||
|
|
f95736339a | ||
|
|
3c83a3c064 | ||
|
|
0928e8d033 | ||
|
|
3b9ea8ebd5 | ||
|
|
b2e7af64b3 | ||
|
|
11ce6be578 | ||
|
|
524e3b46d3 | ||
|
|
000682b4db | ||
| 147d38d5c5 | |||
|
|
14a90f3dc0 | ||
| dcc7c731c6 | |||
| ad77d73363 | |||
| 80ee5bfdda | |||
| e93c5d3167 | |||
| 5ec0f19c53 | |||
| 24da9baffe | |||
| de16ece2ea | |||
| d42ba72170 | |||
| 64563b2f40 | |||
| 18dcf8a139 | |||
| 72ecb314e8 | |||
| 0ce6de8b13 | |||
| fa30c526b9 | |||
| c44fd014e9 | |||
| d44f5216f8 | |||
|
|
f1e8485eb5 | ||
|
|
b57415c846 | ||
|
|
ff06d05857 | ||
|
|
1a4d22ccf0 | ||
|
|
279ac076b0 | ||
|
|
e3301cc200 | ||
|
|
ccf2c68340 | ||
|
|
dc19d3975a | ||
|
|
2eaf1b3c36 | ||
|
|
16cc2f071e | ||
|
|
ae08ee02f4 | ||
| 4f3ccd9c90 | |||
| aaccd9b562 | |||
| 349e1e5332 | |||
| 08e66c95e3 | |||
| 873be55b8b | |||
| c20fb0827f | |||
| 1c0ad51914 | |||
| d81ec5c2fc | |||
| 4bc65ec171 | |||
| e630bc82ee | |||
| 36314e0e3b | |||
| cdc1fc9224 | |||
| be76f4bcc7 | |||
| a71bc0f9c3 | |||
| 3d95d03813 | |||
| d1720f3356 | |||
| 8a317c590b | |||
| 9ab86cfab1 | |||
| 01a3918502 | |||
| 7a014a7aab | |||
| 618089b17e | |||
| 96dbe40637 | |||
|
|
13bc06cc6d | ||
|
|
429b49111a | ||
|
|
6e595d21aa | ||
|
|
80043dc652 | ||
|
|
b470bf6996 | ||
|
|
e0bf48805a | ||
| bb2caa2dda | |||
|
|
dad74ddf4e | ||
| cbf31aecd5 | |||
| 6aab174618 | |||
| 8f2b61d28a | |||
| fb6781e17f | |||
| 1683d8c48b | |||
| e9f95ad3d8 | |||
| 745b83e945 | |||
| b0dec80364 | |||
|
|
03e4ab9d60 | ||
| 9854c9aafa | |||
| b161e319bd | |||
|
|
ae0d1a3c67 | ||
|
|
3da4a395dd | ||
|
|
77a1d67af3 | ||
|
|
856d6cda24 | ||
|
|
cbbea7f810 | ||
|
|
cedaa27a2c | ||
| 72b7a9d7b1 |
108
.eslintrc
108
.eslintrc
@@ -4,12 +4,9 @@
|
|||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"webextensions": true
|
"webextensions": true,
|
||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": ["*.html", "tsconfig.json"],
|
||||||
"*.html",
|
|
||||||
"tsconfig.json"
|
|
||||||
],
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
@@ -21,18 +18,14 @@
|
|||||||
"@unocss",
|
"@unocss",
|
||||||
"prettier",
|
"prettier",
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": ["import", "jsdoc", "react-prefer-function-component", "@typescript-eslint", "simple-import-sort"],
|
||||||
"import",
|
|
||||||
"jsdoc",
|
|
||||||
"react-prefer-function-component"
|
|
||||||
],
|
|
||||||
"globals": {
|
"globals": {
|
||||||
"Atomics": "readonly",
|
"Atomics": "readonly",
|
||||||
"SharedArrayBuffer": "readonly",
|
"SharedArrayBuffer": "readonly",
|
||||||
"debugger": true,
|
"debugger": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"context": true,
|
"context": true,
|
||||||
"JSX": true
|
"JSX": true,
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
@@ -42,37 +35,35 @@
|
|||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
"jsx": true,
|
"jsx": true,
|
||||||
"modules": true,
|
"modules": true,
|
||||||
"experimentalObjectRestSpread": true
|
"experimentalObjectRestSpread": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
"version": "detect"
|
"version": "detect",
|
||||||
},
|
},
|
||||||
"jsdoc": {
|
"jsdoc": {
|
||||||
"mode": "typescript"
|
"mode": "typescript",
|
||||||
},
|
},
|
||||||
"import/parsers": {
|
"import/parsers": {
|
||||||
"@typescript-eslint/parser": [
|
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||||
".ts",
|
|
||||||
".tsx"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"alwaysTryTypes": true,
|
"alwaysTryTypes": true,
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"prefer-const": [
|
"prefer-const": [
|
||||||
"off",
|
"off",
|
||||||
{
|
{
|
||||||
"destructuring": "any",
|
"destructuring": "any",
|
||||||
"ignoreReadBeforeAssign": false
|
"ignoreReadBeforeAssign": false,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
"no-plusplus": "off",
|
||||||
"no-inner-declarations": "off",
|
"no-inner-declarations": "off",
|
||||||
"sort-imports": "off",
|
"sort-imports": "off",
|
||||||
"no-case-declarations": "off",
|
"no-case-declarations": "off",
|
||||||
@@ -82,20 +73,16 @@
|
|||||||
"no-undef": "off",
|
"no-undef": "off",
|
||||||
"no-return-await": "off",
|
"no-return-await": "off",
|
||||||
"@typescript-eslint/return-await": "off",
|
"@typescript-eslint/return-await": "off",
|
||||||
"@typescript-eslint/no-shadow": [
|
"@typescript-eslint/no-shadow": ["off"],
|
||||||
"off"
|
"@typescript-eslint/no-use-before-define": ["off"],
|
||||||
],
|
|
||||||
"@typescript-eslint/no-use-before-define": [
|
|
||||||
"off"
|
|
||||||
],
|
|
||||||
"class-methods-use-this": "off",
|
"class-methods-use-this": "off",
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
"@typescript-eslint/lines-between-class-members": "off",
|
"@typescript-eslint/lines-between-class-members": "off",
|
||||||
"no-param-reassign": [
|
"no-param-reassign": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"props": false
|
"props": false,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"consistent-return": "off",
|
"consistent-return": "off",
|
||||||
@@ -109,8 +96,8 @@
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"before": true,
|
"before": true,
|
||||||
"after": true
|
"after": true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"no-continue": "off",
|
"no-continue": "off",
|
||||||
"space-before-blocks": [
|
"space-before-blocks": [
|
||||||
@@ -118,24 +105,22 @@
|
|||||||
{
|
{
|
||||||
"functions": "always",
|
"functions": "always",
|
||||||
"keywords": "always",
|
"keywords": "always",
|
||||||
"classes": "always"
|
"classes": "always",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"react/jsx-filename-extension": [
|
"react/jsx-filename-extension": [
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
"extensions": [
|
"extensions": [".tsx"],
|
||||||
".tsx"
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"react/no-deprecated": "warn",
|
"react/no-deprecated": "warn",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"react-prefer-function-component/react-prefer-function-component": [
|
"react-prefer-function-component/react-prefer-function-component": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
"allowComponentDidCatch": false
|
"allowComponentDidCatch": false,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"react/function-component-definition": "off",
|
"react/function-component-definition": "off",
|
||||||
"react/button-has-type": "off",
|
"react/button-has-type": "off",
|
||||||
@@ -153,7 +138,7 @@
|
|||||||
"ArrowFunctionExpression": true,
|
"ArrowFunctionExpression": true,
|
||||||
"ClassDeclaration": true,
|
"ClassDeclaration": true,
|
||||||
"ClassExpression": true,
|
"ClassExpression": true,
|
||||||
"FunctionExpression": true
|
"FunctionExpression": true,
|
||||||
},
|
},
|
||||||
"contexts": [
|
"contexts": [
|
||||||
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
|
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
|
||||||
@@ -168,9 +153,9 @@
|
|||||||
"TSInterfaceDeclaration",
|
"TSInterfaceDeclaration",
|
||||||
"TSMethodSignature",
|
"TSMethodSignature",
|
||||||
"TSModuleDeclaration",
|
"TSModuleDeclaration",
|
||||||
"TSTypeAliasDeclaration"
|
"TSTypeAliasDeclaration",
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
@@ -185,31 +170,36 @@
|
|||||||
{
|
{
|
||||||
"target": "./src/background",
|
"target": "./src/background",
|
||||||
"from": "./src/views",
|
"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!"
|
"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",
|
"target": "./src/views",
|
||||||
"from": "./src/background",
|
"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!"
|
"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",
|
"target": "./src/shared",
|
||||||
"from": "./",
|
"from": "./",
|
||||||
"except": [
|
"except": ["./src/shared", "./node_modules"],
|
||||||
"./src/shared",
|
"message": "You cannot import into `shared` from an external directory.",
|
||||||
"./node_modules"
|
},
|
||||||
],
|
],
|
||||||
"message": "You cannot import into `shared` from an external directory."
|
},
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"import/extensions": "off",
|
"import/extensions": "off",
|
||||||
"no-restricted-syntax": [
|
"no-restricted-syntax": [
|
||||||
"error",
|
"error",
|
||||||
"ForInStatement",
|
"ForInStatement",
|
||||||
"LabeledStatement",
|
"LabeledStatement",
|
||||||
"WithStatement"
|
"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",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
43
.github/workflows/best-practices.yml
vendored
Normal file
43
.github/workflows/best-practices.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Best Practices
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
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: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: pnpm run lint
|
||||||
|
format:
|
||||||
|
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: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Run Prettier
|
||||||
|
run: pnpm run prettier
|
||||||
42
.github/workflows/chromatic.yml
vendored
42
.github/workflows/chromatic.yml
vendored
@@ -1,26 +1,26 @@
|
|||||||
name: "Chromatic"
|
name: 'Chromatic'
|
||||||
|
|
||||||
on: [push, pull_request_target]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
chromatic:
|
chromatic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
uses: chromaui/action@latest
|
uses: chromaui/action@latest
|
||||||
with:
|
with:
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
exitZeroOnChanges: true
|
exitZeroOnChanges: true
|
||||||
autoAcceptChanges: "main"
|
autoAcceptChanges: 'main'
|
||||||
|
|||||||
24
.github/workflows/tests.yml
vendored
Normal file
24
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
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: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pnpm test
|
||||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no -- commitlint --edit $1
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// .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/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
@@ -34,4 +37,7 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
}
|
"tailwindCSS.includeLanguages": {
|
||||||
|
"plaintext": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -2,17 +2,29 @@
|
|||||||
|
|
||||||
## Built Using
|
## Built Using
|
||||||
|
|
||||||
- React 18
|
- React 18
|
||||||
- TypeScript
|
- TypeScript
|
||||||
- Vite 5
|
- Vite 5
|
||||||
- ESLint
|
- ESLint
|
||||||
- Prettier
|
- Prettier
|
||||||
- Semantic-Release
|
- Semantic-Release
|
||||||
- Custom Messaging & Storage Wrappers
|
- Custom Messaging & Storage Wrappers
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Clone this repo
|
1. Clone this repo
|
||||||
2. Run `pnpm install` to install and patch all the required dependencies
|
2. Run `pnpm install` to install and patch all the required dependencies
|
||||||
3. Run `pnpm run dev` to start the development server
|
|
||||||
4. Run `pnpm build` to build the extension for production
|
- If you want to run the development build:
|
||||||
|
|
||||||
|
- Run `pnpm run dev`
|
||||||
|
|
||||||
|
- If you want to build the extension for production:
|
||||||
|
|
||||||
|
- Run `pnpm build`
|
||||||
|
|
||||||
|
You may have to rename the `__uno.css.js` to `uno.css.js` in dist
|
||||||
|
|
||||||
|
Go to chrome://extensions, ensure you have "Developer Mode" enabled, and click 'Load unpacked'
|
||||||
|
|
||||||
|
Navigate to the 'dist' folder and click 'select' to import the extension
|
||||||
|
|||||||
123
commitlint.config.ts
Normal file
123
commitlint.config.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { RuleConfigCondition, RuleConfigSeverity, TargetCaseType } from '@commitlint/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
parserPreset: 'conventional-changelog-conventionalcommits',
|
||||||
|
rules: {
|
||||||
|
'body-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
|
||||||
|
'body-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
|
||||||
|
'footer-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
|
||||||
|
'footer-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
|
||||||
|
'header-max-length': [RuleConfigSeverity.Error, 'always', 100] as const,
|
||||||
|
'header-trim': [RuleConfigSeverity.Error, 'always'] as const,
|
||||||
|
'subject-case': [
|
||||||
|
RuleConfigSeverity.Error,
|
||||||
|
'never',
|
||||||
|
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
|
||||||
|
] as [RuleConfigSeverity, RuleConfigCondition, TargetCaseType[]],
|
||||||
|
'subject-empty': [RuleConfigSeverity.Error, 'never'] as const,
|
||||||
|
'subject-full-stop': [RuleConfigSeverity.Error, 'never', '.'] as const,
|
||||||
|
'type-case': [RuleConfigSeverity.Error, 'always', 'lower-case'] as const,
|
||||||
|
'type-empty': [RuleConfigSeverity.Error, 'never'] as const,
|
||||||
|
'type-enum': [
|
||||||
|
RuleConfigSeverity.Error,
|
||||||
|
'always',
|
||||||
|
['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
|
||||||
|
] as [RuleConfigSeverity, RuleConfigCondition, string[]],
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
questions: {
|
||||||
|
type: {
|
||||||
|
description: "Select the type of change that you're committing",
|
||||||
|
enum: {
|
||||||
|
feat: {
|
||||||
|
description: 'A new feature',
|
||||||
|
title: 'Features',
|
||||||
|
emoji: '✨',
|
||||||
|
},
|
||||||
|
fix: {
|
||||||
|
description: 'A bug fix',
|
||||||
|
title: 'Bug Fixes',
|
||||||
|
emoji: '🐛',
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: 'Documentation only changes',
|
||||||
|
title: 'Documentation',
|
||||||
|
emoji: '📚',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
description:
|
||||||
|
'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
|
||||||
|
title: 'Styles',
|
||||||
|
emoji: '💎',
|
||||||
|
},
|
||||||
|
refactor: {
|
||||||
|
description: 'A code change that neither fixes a bug nor adds a feature',
|
||||||
|
title: 'Code Refactoring',
|
||||||
|
emoji: '📦',
|
||||||
|
},
|
||||||
|
perf: {
|
||||||
|
description: 'A code change that improves performance',
|
||||||
|
title: 'Performance Improvements',
|
||||||
|
emoji: '🚀',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
description: 'Adding missing tests or correcting existing tests',
|
||||||
|
title: 'Tests',
|
||||||
|
emoji: '🚨',
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
description:
|
||||||
|
'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
|
||||||
|
title: 'Builds',
|
||||||
|
emoji: '🛠',
|
||||||
|
},
|
||||||
|
ci: {
|
||||||
|
description:
|
||||||
|
'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
|
||||||
|
title: 'Continuous Integrations',
|
||||||
|
emoji: '⚙️',
|
||||||
|
},
|
||||||
|
chore: {
|
||||||
|
description: "Other changes that don't modify src or test files",
|
||||||
|
title: 'Chores',
|
||||||
|
emoji: '♻️',
|
||||||
|
},
|
||||||
|
revert: {
|
||||||
|
description: 'Reverts a previous commit',
|
||||||
|
title: 'Reverts',
|
||||||
|
emoji: '🗑',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
description: 'What is the scope of this change (e.g. component or file name)',
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
description: 'Write a short, imperative tense description of the change',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
description: 'Provide a longer description of the change',
|
||||||
|
},
|
||||||
|
isBreaking: {
|
||||||
|
description: 'Are there any breaking changes?',
|
||||||
|
},
|
||||||
|
breakingBody: {
|
||||||
|
description:
|
||||||
|
'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself',
|
||||||
|
},
|
||||||
|
breaking: {
|
||||||
|
description: 'Describe the breaking changes',
|
||||||
|
},
|
||||||
|
isIssueAffected: {
|
||||||
|
description: 'Does this change affect any open issues?',
|
||||||
|
},
|
||||||
|
issuesBody: {
|
||||||
|
description:
|
||||||
|
'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself',
|
||||||
|
},
|
||||||
|
issues: {
|
||||||
|
description: 'Add issue references (e.g. "fix #123", "re #123".)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
3267
package-lock.json
generated
3267
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@@ -9,60 +9,77 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"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": "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",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"coverage": "vitest run --coverage",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"devtools": "react-devtools",
|
"devtools": "react-devtools",
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build"
|
"build-storybook": "storybook build",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^1.7.18",
|
||||||
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
|
"@hello-pangea/dnd": "^16.5.0",
|
||||||
"@types/sql.js": "^1.4.9",
|
"@types/sql.js": "^1.4.9",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"chrome-extension-toolkit": "^0.0.51",
|
"chrome-extension-toolkit": "^0.0.51",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"highcharts": "^11.3.0",
|
"highcharts": "^11.3.0",
|
||||||
"highcharts-react-official": "^3.2.1",
|
"highcharts-react-official": "^3.2.1",
|
||||||
|
"html-to-image": "^1.11.11",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-devtools-core": "^5.0.0",
|
"react-devtools-core": "^5.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"sass": "^1.70.0",
|
"react-window": "^1.8.10",
|
||||||
|
"sass": "^1.71.1",
|
||||||
"sql.js": "1.10.2",
|
"sql.js": "1.10.2",
|
||||||
|
"styled-components": "^6.1.8",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^18.6.1",
|
||||||
|
"@commitlint/config-conventional": "^18.6.2",
|
||||||
"@crxjs/vite-plugin": "2.0.0-beta.21",
|
"@crxjs/vite-plugin": "2.0.0-beta.21",
|
||||||
"@iconify-json/material-symbols": "^1.1.72",
|
"@iconify-json/material-symbols": "^1.1.73",
|
||||||
"@storybook/addon-designs": "^7.0.9",
|
"@storybook/addon-designs": "^7.0.9",
|
||||||
"@storybook/addon-essentials": "^7.6.12",
|
"@storybook/addon-essentials": "^7.6.17",
|
||||||
"@storybook/addon-links": "^7.6.12",
|
"@storybook/addon-links": "^7.6.17",
|
||||||
"@storybook/blocks": "^7.6.12",
|
"@storybook/blocks": "^7.6.17",
|
||||||
"@storybook/react": "^7.6.12",
|
"@storybook/react": "^7.6.17",
|
||||||
"@storybook/react-vite": "^7.6.12",
|
"@storybook/react-vite": "^7.6.17",
|
||||||
"@storybook/test": "^7.6.12",
|
"@storybook/test": "^7.6.17",
|
||||||
"@svgr/core": "^8.1.0",
|
"@svgr/core": "^8.1.0",
|
||||||
"@svgr/plugin-jsx": "^8.1.0",
|
"@svgr/plugin-jsx": "^8.1.0",
|
||||||
"@types/chrome": "^0.0.260",
|
"@types/chrome": "^0.0.260",
|
||||||
"@types/node": "^20.11.16",
|
"@types/node": "^20.11.19",
|
||||||
"@types/prompts": "^2.4.9",
|
"@types/prompts": "^2.4.9",
|
||||||
"@types/react": "^18.2.51",
|
"@types/react": "^18.2.57",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@types/semver": "^7.5.6",
|
"@types/semver": "^7.5.7",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@unocss/eslint-config": "^0.58.4",
|
"@unocss/eslint-config": "^0.58.5",
|
||||||
"@unocss/postcss": "^0.58.4",
|
"@unocss/postcss": "^0.58.5",
|
||||||
"@unocss/preset-uno": "^0.58.4",
|
"@unocss/preset-uno": "^0.58.5",
|
||||||
"@unocss/preset-web-fonts": "^0.58.4",
|
"@unocss/preset-web-fonts": "^0.58.5",
|
||||||
"@unocss/reset": "^0.58.5",
|
"@unocss/reset": "^0.58.5",
|
||||||
"@unocss/transformer-directives": "^0.58.4",
|
"@unocss/transformer-directives": "^0.58.5",
|
||||||
"@unocss/transformer-variant-group": "^0.58.4",
|
"@unocss/transformer-variant-group": "^0.58.5",
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||||
"chromatic": "^10.9.1",
|
"@vitest/coverage-v8": "^1.3.1",
|
||||||
|
"@vitest/ui": "^1.3.1",
|
||||||
|
"chromatic": "^10.9.6",
|
||||||
"cssnano": "^6.0.3",
|
"cssnano": "^6.0.3",
|
||||||
"cssnano-preset-advanced": "^6.0.3",
|
"cssnano-preset-advanced": "^6.0.3",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.5",
|
||||||
"es-module-lexer": "^1.4.1",
|
"es-module-lexer": "^1.4.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
@@ -71,24 +88,27 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsdoc": "^48.0.4",
|
"eslint-plugin-jsdoc": "^48.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||||
"eslint-plugin-storybook": "^0.6.15",
|
"eslint-plugin-storybook": "^0.6.15",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"react-dev-utils": "^12.0.1",
|
"react-dev-utils": "^12.0.1",
|
||||||
"react-devtools": "^5.0.0",
|
"react-devtools": "^5.0.0",
|
||||||
"storybook": "^7.6.12",
|
"storybook": "^7.6.17",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"unocss": "^0.58.4",
|
"unocss": "^0.58.5",
|
||||||
"unplugin-icons": "^0.18.3",
|
"unplugin-icons": "^0.18.5",
|
||||||
"vite": "^5.0.12",
|
"vite": "^5.1.4",
|
||||||
"vite-plugin-inspect": "^0.8.3"
|
"vite-plugin-inspect": "^0.8.3",
|
||||||
|
"vitest": "^1.3.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
|||||||
2999
pnpm-lock.yaml
generated
2999
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
8
src/assets/icons/cal.svg
Normal file
8
src/assets/icons/cal.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
8
src/assets/icons/png.svg
Normal file
8
src/assets/icons/png.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 702 B |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -14,6 +14,7 @@ export default async function createSchedule(scheduleName: string): Promise<stri
|
|||||||
schedules.push({
|
schedules.push({
|
||||||
name: scheduleName,
|
name: scheduleName,
|
||||||
courses: [],
|
courses: [],
|
||||||
|
hours: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
|
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar page
|
||||||
|
* @returns entire page
|
||||||
|
*/
|
||||||
export default function CalendarMain() {
|
export default function CalendarMain() {
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
<div>Calendar Placeholder</div>
|
<Calendar />
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
|
|||||||
new UserSchedule({
|
new UserSchedule({
|
||||||
courses: [],
|
courses: [],
|
||||||
name: 'Schedule 1',
|
name: 'Schedule 1',
|
||||||
|
hours: 0,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
activeIndex: 0,
|
activeIndex: 0,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Serialized } from 'chrome-extension-toolkit';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* a map of the days of the week that a class is taught, and the corresponding abbreviation
|
* a map of the days of the week that a class is taught, and the corresponding abbreviation
|
||||||
|
* Don't modify the keys
|
||||||
*/
|
*/
|
||||||
export const DAY_MAP = {
|
export const DAY_MAP = {
|
||||||
M: 'Monday',
|
M: 'Monday',
|
||||||
@@ -14,7 +15,7 @@ export const DAY_MAP = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** A day of the week that a class is taught */
|
/** A day of the week that a class is taught */
|
||||||
export type Day = typeof DAY_MAP[keyof typeof DAY_MAP];
|
export type Day = (typeof DAY_MAP)[keyof typeof DAY_MAP];
|
||||||
|
|
||||||
/** A physical room that a class is taught in */
|
/** A physical room that a class is taught in */
|
||||||
export type Location = {
|
export type Location = {
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ import { Course } from './Course';
|
|||||||
export class UserSchedule {
|
export class UserSchedule {
|
||||||
courses: Course[];
|
courses: Course[];
|
||||||
name: string;
|
name: string;
|
||||||
|
hours: number;
|
||||||
|
|
||||||
constructor(schedule: Serialized<UserSchedule>) {
|
constructor(schedule: Serialized<UserSchedule>) {
|
||||||
this.courses = schedule.courses.map(c => new Course(c));
|
this.courses = schedule.courses.map(c => new Course(c));
|
||||||
this.name = schedule.name;
|
this.name = schedule.name;
|
||||||
|
this.hours = 0;
|
||||||
|
for (const course of this.courses) {
|
||||||
|
this.hours += course.creditHours;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
containsCourse(course: Course): boolean {
|
containsCourse(course: Course): boolean {
|
||||||
return this.courses.some(c => c.uniqueId === course.uniqueId);
|
return this.courses.some(c => c.uniqueId === course.uniqueId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ export interface CourseColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculates luminance of a hex string
|
// calculates luminance of a hex string
|
||||||
function getLuminance(hex: string): number {
|
export function getLuminance(hex: string): number {
|
||||||
let r = parseInt(hex.substring(1, 3), 16);
|
let r = parseInt(hex.substring(1, 3), 16);
|
||||||
let g = parseInt(hex.substring(3, 5), 16);
|
let g = parseInt(hex.substring(3, 5), 16);
|
||||||
let b = parseInt(hex.substring(5, 7), 16);
|
let b = parseInt(hex.substring(5, 7), 16);
|
||||||
@@ -42,9 +42,9 @@ export function pickFontColor(bgColor: string): 'text-white' | 'text-black' {
|
|||||||
* Get primary and secondary colors from a tailwind colorway
|
* Get primary and secondary colors from a tailwind colorway
|
||||||
* @param colorway the tailwind colorway ex. "emerald"
|
* @param colorway the tailwind colorway ex. "emerald"
|
||||||
*/
|
*/
|
||||||
export function getCourseColors(colorway: keyof typeof theme.colors): CourseColors {
|
export function getCourseColors(colorway: keyof typeof theme.colors, index = 600, offset = 200): CourseColors {
|
||||||
return {
|
return {
|
||||||
primaryColor: theme.colors[colorway][600] as string,
|
primaryColor: theme.colors[colorway][index] as string,
|
||||||
secondaryColor: theme.colors[colorway][800] as string,
|
secondaryColor: theme.colors[colorway][index + offset] as string,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/shared/util/tests/colors.test.ts
Normal file
22
src/shared/util/tests/colors.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { getLuminance } from '../colors';
|
||||||
|
|
||||||
|
describe('getLuminance', () => {
|
||||||
|
it('should return the correct luminance value for a given hex color', () => {
|
||||||
|
// Test case 1: Hex color #FFFFFF (white)
|
||||||
|
expect(getLuminance('#FFFFFF')).toBeCloseTo(1);
|
||||||
|
|
||||||
|
// Test case 2: Hex color #000000 (black)
|
||||||
|
expect(getLuminance('#000000')).toBeCloseTo(0);
|
||||||
|
|
||||||
|
// Test case 3: Hex color #FF0000 (red)
|
||||||
|
expect(getLuminance('#FF0000')).toBeCloseTo(0.2126);
|
||||||
|
|
||||||
|
// Test case 4: Hex color #00FF00 (green)
|
||||||
|
expect(getLuminance('#00FF00')).toBeCloseTo(0.7152);
|
||||||
|
|
||||||
|
// Test case 5: Hex color #0000FF (blue)
|
||||||
|
expect(getLuminance('#0000FF')).toBeCloseTo(0.0722);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
src/shared/util/tests/random.test.ts
Normal file
26
src/shared/util/tests/random.test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { generateRandomId } from '../random';
|
||||||
|
|
||||||
|
describe('generateRandomId', () => {
|
||||||
|
it('should generate a random ID with the specified length', () => {
|
||||||
|
// Test case 1: Length 5
|
||||||
|
expect(generateRandomId(5)).toHaveLength(5);
|
||||||
|
|
||||||
|
// Test case 2: Length 10
|
||||||
|
expect(generateRandomId(10)).toHaveLength(10);
|
||||||
|
|
||||||
|
// Test case 3: Length 15
|
||||||
|
expect(generateRandomId(15)).toHaveLength(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a unique ID each time', () => {
|
||||||
|
// Generate 100 IDs and check if they are all unique
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (let i = 0; i < 100; i += 1) {
|
||||||
|
const id = generateRandomId();
|
||||||
|
expect(ids.has(id)).toBe(false);
|
||||||
|
ids.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
39
src/shared/util/tests/string.test.ts
Normal file
39
src/shared/util/tests/string.test.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { capitalize } from '../string';
|
||||||
|
|
||||||
|
// TODO: Fix `string.ts` and `string.test.ts` to make the tests pass
|
||||||
|
// `capitalize` is adding an extra space at the end of the word.
|
||||||
|
describe('capitalize', () => {
|
||||||
|
it('should capitalize the first letter of each word', () => {
|
||||||
|
// Debug
|
||||||
|
const word = 'hello world';
|
||||||
|
const capitalized = capitalize(word);
|
||||||
|
console.log(capitalize(word));
|
||||||
|
console.log(capitalized.length);
|
||||||
|
console.log(capitalized.split(''));
|
||||||
|
|
||||||
|
// Test case 1: Single word
|
||||||
|
expect(capitalize('hello')).toBe('Hello');
|
||||||
|
|
||||||
|
// Test case 2: Multiple words
|
||||||
|
expect(capitalize('hello world')).toBe('Hello World');
|
||||||
|
|
||||||
|
// Test case 3: Words with hyphens
|
||||||
|
expect(capitalize('hello-world')).toBe('Hello-World');
|
||||||
|
|
||||||
|
// Test case 4: Words with hyphens and spaces
|
||||||
|
expect(capitalize('hello-world test')).toBe('Hello-World Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change the capitalization of the remaining letters', () => {
|
||||||
|
// Test case 1: All lowercase
|
||||||
|
expect(capitalize('hello')).toBe('Hello');
|
||||||
|
|
||||||
|
// Test case 2: All uppercase
|
||||||
|
expect(capitalize('WORLD')).toBe('WORLD');
|
||||||
|
|
||||||
|
// Test case 3: Mixed case
|
||||||
|
expect(capitalize('HeLLo WoRLd')).toBe('Hello World');
|
||||||
|
});
|
||||||
|
});
|
||||||
14
src/shared/util/tests/time.test.ts
Normal file
14
src/shared/util/tests/time.test.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { sleep } from '../time';
|
||||||
|
|
||||||
|
describe('sleep', () => {
|
||||||
|
it('should resolve after the specified number of milliseconds', async () => {
|
||||||
|
const start = Date.now();
|
||||||
|
const milliseconds = 1000;
|
||||||
|
await sleep(milliseconds);
|
||||||
|
const end = Date.now();
|
||||||
|
const elapsed = end - start;
|
||||||
|
expect(elapsed).toBeGreaterThanOrEqual(milliseconds);
|
||||||
|
});
|
||||||
|
});
|
||||||
75
src/shared/util/themeColors.ts
Normal file
75
src/shared/util/themeColors.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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: '#af2e2d',
|
||||||
|
black: '#1a2024',
|
||||||
|
},
|
||||||
|
gradeDistribution: {
|
||||||
|
a: '#22c55e',
|
||||||
|
aminus: '#a3e635',
|
||||||
|
bplus: '#84CC16',
|
||||||
|
b: '#FDE047',
|
||||||
|
bminus: '#FACC15',
|
||||||
|
cplus: '#F59E0B',
|
||||||
|
c: '#FB923C',
|
||||||
|
cminus: '#F97316',
|
||||||
|
dplus: '#EA580C', // TODO (achadaga): copilot generated, get actual color from Isaiah
|
||||||
|
d: '#DC2626',
|
||||||
|
dminus: '#B91C1C',
|
||||||
|
f: '#B91C1C',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type NestedKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends Record<string, any> ? `${string & K}-${string & keyof T[K]}` : never;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A union of all colors in the theme
|
||||||
|
*/
|
||||||
|
export type ThemeColor = NestedKeys<typeof colors>;
|
||||||
|
|
||||||
|
export const colorsFlattened = Object.entries(colors).reduce(
|
||||||
|
(acc, [prefix, group]) => {
|
||||||
|
for (const [name, hex] of Object.entries(group)) {
|
||||||
|
acc[`${prefix}-${name}`] = hex;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<ThemeColor, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
const hexToRgb = (hex: string) =>
|
||||||
|
hex.match(/[0-9a-f]{2}/gi).map(partialHex => parseInt(partialHex, 16)) as [number, number, number];
|
||||||
|
|
||||||
|
const colorsFlattenedRgb = Object.fromEntries(
|
||||||
|
Object.entries(colorsFlattened).map(([name, hex]) => [name, hexToRgb(hex)])
|
||||||
|
) as Record<ThemeColor, ReturnType<typeof hexToRgb>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the hexadecimal color value by name from the theme.
|
||||||
|
*
|
||||||
|
* @param name - The name of the theme color.
|
||||||
|
* @returns The hexadecimal color value.
|
||||||
|
*/
|
||||||
|
export const getThemeColorHexByName = (name: ThemeColor): string => colorsFlattened[name];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param name - The name of the theme color.
|
||||||
|
* @returns An array of the red, green, and blue values, respectively
|
||||||
|
*/
|
||||||
|
export const getThemeColorRgbByName = (name: ThemeColor) => colorsFlattenedRgb[name];
|
||||||
@@ -1,24 +1,33 @@
|
|||||||
import { Button } from 'src/views/components/common/Button/Button';
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { colorsFlattened } from 'src/shared/util/themeColors';
|
||||||
|
import { Button } from 'src/views/components/common/Button/Button';
|
||||||
|
import AddIcon from '~icons/material-symbols/add';
|
||||||
|
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
|
||||||
|
import DescriptionIcon from '~icons/material-symbols/description';
|
||||||
|
import ImagePlaceholderIcon from '~icons/material-symbols/image';
|
||||||
|
import HappyFaceIcon from '~icons/material-symbols/mood';
|
||||||
|
import RemoveIcon from '~icons/material-symbols/remove';
|
||||||
|
import ReviewsIcon from '~icons/material-symbols/reviews';
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/Button',
|
title: 'Components/Common/Button',
|
||||||
component: Button,
|
component: Button,
|
||||||
parameters: {
|
parameters: {
|
||||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||||
args: {
|
args: {
|
||||||
children: 'Button',
|
children: 'Button',
|
||||||
},
|
icon: ImagePlaceholderIcon,
|
||||||
argTypes: {
|
},
|
||||||
children: { control: 'text' },
|
argTypes: {
|
||||||
},
|
children: { control: 'text' },
|
||||||
|
},
|
||||||
} satisfies Meta<typeof Button>;
|
} satisfies Meta<typeof Button>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@@ -26,72 +35,112 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {},
|
args: {
|
||||||
|
variant: 'filled',
|
||||||
|
color: 'ut-black',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: {
|
args: {
|
||||||
disabled: true,
|
variant: 'filled',
|
||||||
},
|
color: 'ut-black',
|
||||||
};
|
disabled: true,
|
||||||
|
|
||||||
export const Grid: Story = {
|
|
||||||
render: props => (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Button {...props} type='primary' />
|
|
||||||
<Button {...props} type='secondary' />
|
|
||||||
<Button {...props} type='tertiary' />
|
|
||||||
<Button {...props} type='danger' />
|
|
||||||
<Button {...props} type='warning' />
|
|
||||||
<Button {...props} type='success' />
|
|
||||||
<Button {...props} type='info' />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Button {...props} type='primary' disabled />
|
|
||||||
<Button {...props} type='secondary' disabled />
|
|
||||||
<Button {...props} type='tertiary' disabled />
|
|
||||||
<Button {...props} type='danger' disabled />
|
|
||||||
<Button {...props} type='warning' disabled />
|
|
||||||
<Button {...props} type='success' disabled />
|
|
||||||
<Button {...props} type='info' disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: Actually show the buttons as they appear in the design
|
|
||||||
export const CourseButtons: Story = {
|
|
||||||
args: {
|
|
||||||
children: 'Add Course',
|
|
||||||
},
|
|
||||||
render: props => (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Button {...props} type='primary' />
|
|
||||||
<Button {...props} type='secondary' />
|
|
||||||
<Button {...props} type='tertiary' />
|
|
||||||
<Button {...props} type='danger' />
|
|
||||||
<Button {...props} type='warning' />
|
|
||||||
<Button {...props} type='success' />
|
|
||||||
<Button {...props} type='info' />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Button {...props} type='primary' disabled />
|
|
||||||
<Button {...props} type='secondary' disabled />
|
|
||||||
<Button {...props} type='tertiary' disabled />
|
|
||||||
<Button {...props} type='danger' disabled />
|
|
||||||
<Button {...props} type='warning' disabled />
|
|
||||||
<Button {...props} type='success' disabled />
|
|
||||||
<Button {...props} type='info' disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
parameters: {
|
|
||||||
design: {
|
|
||||||
type: 'figma',
|
|
||||||
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=324-389&mode=design&t=BoS5xBrpSsjgQXqv-4',
|
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export const Grid: Story = {
|
||||||
|
render: props => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '15px' }}>
|
||||||
|
<Button {...props} variant='filled' color='ut-black' />
|
||||||
|
<Button {...props} variant='outline' color='ut-black' />
|
||||||
|
<Button {...props} variant='single' color='ut-black' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '15px' }}>
|
||||||
|
<Button {...props} variant='filled' color='ut-black' disabled />
|
||||||
|
<Button {...props} variant='outline' color='ut-black' disabled />
|
||||||
|
<Button {...props} variant='single' color='ut-black' disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PrettyColors: Story = {
|
||||||
|
// @ts-ignore
|
||||||
|
args: {
|
||||||
|
children: '',
|
||||||
|
},
|
||||||
|
render: props => {
|
||||||
|
const colorsNames = Object.keys(colorsFlattened) as (keyof typeof colorsFlattened)[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
|
||||||
|
{colorsNames.map(color => (
|
||||||
|
<div style={{ display: 'flex', gap: '15px' }} key={color}>
|
||||||
|
<Button {...props} variant='filled' color={color}>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='outline' color={color}>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='single' color={color}>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='filled' color={color} />
|
||||||
|
<Button {...props} variant='outline' color={color} />
|
||||||
|
<Button {...props} variant='single' color={color} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export const CourseButtons: Story = {
|
||||||
|
render: props => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', alignItems: 'center' }}>
|
||||||
|
<Button {...props} variant='filled' color='ut-green' icon={AddIcon}>
|
||||||
|
Add Course
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='filled' color='theme-red' icon={RemoveIcon}>
|
||||||
|
Remove Course
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=324-389&mode=design&t=BoS5xBrpSsjgQXqv-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CourseCatalogActionButtons: Story = {
|
||||||
|
// @ts-ignore
|
||||||
|
args: {
|
||||||
|
children: '',
|
||||||
|
},
|
||||||
|
render: props => (
|
||||||
|
<div style={{ display: 'flex', gap: '15px' }}>
|
||||||
|
<Button {...props} variant='filled' color='ut-burntorange' icon={CalendarMonthIcon} />
|
||||||
|
<Button {...props} variant='outline' color='ut-blue' icon={ReviewsIcon}>
|
||||||
|
RateMyProf
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='outline' color='ut-teal' icon={HappyFaceIcon}>
|
||||||
|
CES
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='outline' color='ut-yellow' icon={DescriptionIcon}>
|
||||||
|
Past Syllabi
|
||||||
|
</Button>
|
||||||
|
<Button {...props} variant='filled' color='ut-green' icon={AddIcon}>
|
||||||
|
Add Course
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
// Calendar.stories.tsx
|
|
||||||
import React from 'react';
|
|
||||||
import Calendar from '@views/components/common/CalendarGrid/CalendarGrid';
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'Components/Common/Calendar',
|
|
||||||
component: Calendar,
|
|
||||||
parameters: {
|
|
||||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
|
||||||
layout: 'centered',
|
|
||||||
tags: ['autodocs'],
|
|
||||||
}
|
|
||||||
} satisfies Meta<typeof Calendar>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof meta>;
|
|
||||||
|
|
||||||
export const Default: Story = {};
|
|
||||||
23
src/stories/components/Chip.stories.tsx
Normal file
23
src/stories/components/Chip.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Chip } from 'src/views/components/common/Chip/Chip';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/Chip',
|
||||||
|
component: Chip,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
label: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Chip>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'QR',
|
||||||
|
},
|
||||||
|
};
|
||||||
117
src/stories/components/ConflictsWithWarning.stories.tsx
Normal file
117
src/stories/components/ConflictsWithWarning.stories.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
|
||||||
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
|
||||||
|
export const ExampleCourse: Course = new Course({
|
||||||
|
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'C S',
|
||||||
|
description: [
|
||||||
|
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
|
||||||
|
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
|
||||||
|
'May be counted toward the Quantitative Reasoning flag requirement.',
|
||||||
|
'Designed to accommodate 100 or more students.',
|
||||||
|
'Taught as a Web-based course.',
|
||||||
|
],
|
||||||
|
flags: ['Quantitative Reasoning'],
|
||||||
|
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
|
instructionMode: 'Online',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Bevo',
|
||||||
|
lastName: 'Bevo',
|
||||||
|
fullName: 'Bevo Bevo',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '303E',
|
||||||
|
schedule: {
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Tuesday', 'Thursday'],
|
||||||
|
endTime: 660,
|
||||||
|
startTime: 570,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
uniqueId: 12345,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
export const ExampleCourse2: Course = new Course({
|
||||||
|
courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'C S',
|
||||||
|
description: [
|
||||||
|
'Restricted to computer science majors.',
|
||||||
|
'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
|
||||||
|
'Computer Science 439 and 439H may not both be counted.',
|
||||||
|
'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
|
||||||
|
'May be counted toward the Independent Inquiry flag requirement.',
|
||||||
|
],
|
||||||
|
flags: ['Independent Inquiry'],
|
||||||
|
fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Allison',
|
||||||
|
lastName: 'Norman',
|
||||||
|
fullName: 'Allison Norman',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '439',
|
||||||
|
schedule: {
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Tuesday', 'Thursday'],
|
||||||
|
startTime: 930,
|
||||||
|
endTime: 1050,
|
||||||
|
}),
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Friday'],
|
||||||
|
startTime: 600,
|
||||||
|
endTime: 720,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
uniqueId: 67890,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/ConflictsWithWarning',
|
||||||
|
component: ConflictsWithWarning,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
conflicts: { control: 'object' },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
conflicts: [ExampleCourse, ExampleCourse2],
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ConflictsWithWarning>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
conflicts: [ExampleCourse, ExampleCourse2],
|
||||||
|
},
|
||||||
|
};
|
||||||
157
src/stories/components/Dropdown.stories.tsx
Normal file
157
src/stories/components/Dropdown.stories.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import type { Serialized } from 'chrome-extension-toolkit';
|
||||||
|
import React from 'react';
|
||||||
|
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
import Dropdown from 'src/views/components/common/Dropdown/Dropdown';
|
||||||
|
import ScheduleListItem from 'src/views/components/common/ScheduleListItem/ScheduleListItem';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Dropdown> = {
|
||||||
|
title: 'Components/Common/Dropdown',
|
||||||
|
component: Dropdown,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
dummySchedules: { control: 'object' },
|
||||||
|
dummyActiveIndex: { control: 'number' },
|
||||||
|
scheduleComponents: { control: 'object' },
|
||||||
|
},
|
||||||
|
render: (args: any) => (
|
||||||
|
<div className='w-80'>
|
||||||
|
<Dropdown {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof Dropdown>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
const schedules = [
|
||||||
|
new UserSchedule({
|
||||||
|
courses: [
|
||||||
|
new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Fall',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
name: 'Main Schedule',
|
||||||
|
hours: 0,
|
||||||
|
} as Serialized<UserSchedule>),
|
||||||
|
new UserSchedule({
|
||||||
|
courses: [
|
||||||
|
new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Fall',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Fall',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
name: 'Backup #3',
|
||||||
|
hours: 0,
|
||||||
|
} as Serialized<UserSchedule>),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Hidden: Story = {
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1579-5083&mode=dev',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
args: {
|
||||||
|
dummySchedules: schedules,
|
||||||
|
dummyActiveIndex: 0,
|
||||||
|
scheduleComponents: schedules.map((schedule, index) => (
|
||||||
|
<ScheduleListItem active={index === 0} name={schedule.name} />
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
};
|
||||||
19
src/stories/components/ImportantLinks.stories.tsx
Normal file
19
src/stories/components/ImportantLinks.stories.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import ImportantLinks from 'src/views/components/calendar/ImportantLinks';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/ImportantLinks',
|
||||||
|
component: ImportantLinks,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<typeof ImportantLinks>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
25
src/stories/components/InfoCard.stories.tsx
Normal file
25
src/stories/components/InfoCard.stories.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { InfoCard } from 'src/views/components/common/InfoCard/InfoCard';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/InfoCard',
|
||||||
|
component: InfoCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
titleText: { control: 'text' },
|
||||||
|
bodyText: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof InfoCard>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
titleText: 'WAITLIST SIZE',
|
||||||
|
bodyText: '14 Students',
|
||||||
|
},
|
||||||
|
};
|
||||||
96
src/stories/components/List.stories.tsx
Normal file
96
src/stories/components/List.stories.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||||
|
import Instructor from '@shared/types/Instructor';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import List from '@views/components/common/List/List';
|
||||||
|
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
|
||||||
|
import React from 'react';
|
||||||
|
import { test_colors } from './PopupCourseBlock.stories';
|
||||||
|
|
||||||
|
const numberOfCourses = 5;
|
||||||
|
|
||||||
|
export const generateCourses = count => {
|
||||||
|
const courses = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const course = new Course({
|
||||||
|
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'C S',
|
||||||
|
description: [
|
||||||
|
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
|
||||||
|
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
|
||||||
|
'May be counted toward the Quantitative Reasoning flag requirement.',
|
||||||
|
'Designed to accommodate 100 or more students.',
|
||||||
|
'Taught as a Web-based course.',
|
||||||
|
],
|
||||||
|
flags: ['Quantitative Reasoning'],
|
||||||
|
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
|
instructionMode: 'Online',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Bevo',
|
||||||
|
lastName: 'Bevo',
|
||||||
|
fullName: 'Bevo Bevo',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '303E',
|
||||||
|
schedule: {
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Tuesday', 'Thursday'],
|
||||||
|
endTime: 660,
|
||||||
|
startTime: 570,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
uniqueId: 12345 + i, // Make uniqueId different for each course
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
|
courses.push(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
return courses;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exampleCourses = generateCourses(numberOfCourses);
|
||||||
|
const generateCourseBlocks = (exampleCourses, colors) =>
|
||||||
|
exampleCourses.map((course, i) => <PopupCourseBlock key={course.uniqueId} course={course} colors={colors[i]} />);
|
||||||
|
export const exampleCourseBlocks = generateCourseBlocks(exampleCourses, test_colors);
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/List',
|
||||||
|
component: List,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
draggableElements: { control: 'object' },
|
||||||
|
itemHeight: { control: 'number' },
|
||||||
|
listHeight: { control: 'number' },
|
||||||
|
listWidth: { control: 'number' },
|
||||||
|
gap: { control: 'number' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof List>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
draggableElements: exampleCourseBlocks,
|
||||||
|
itemHeight: 55,
|
||||||
|
listHeight: 300,
|
||||||
|
listWidth: 300,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Course, Status } from 'src/shared/types/Course';
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
import Instructor from 'src/shared/types/Instructor';
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
|
|
||||||
import { getCourseColors } from 'src/shared/util/colors';
|
import { getCourseColors } from 'src/shared/util/colors';
|
||||||
import { theme } from 'unocss/preset-mini';
|
import { theme } from 'unocss/preset-mini';
|
||||||
|
|
||||||
const exampleCourse: Course = new Course({
|
export const exampleCourse: Course = new Course({
|
||||||
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
creditHours: 3,
|
creditHours: 3,
|
||||||
department: 'C S',
|
department: 'C S',
|
||||||
@@ -95,7 +95,7 @@ export const Variants: Story = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = Object.keys(theme.colors)
|
export const test_colors = Object.keys(theme.colors)
|
||||||
// check that the color is a colorway (is an object)
|
// check that the color is a colorway (is an object)
|
||||||
.filter(color => typeof theme.colors[color] === 'object')
|
.filter(color => typeof theme.colors[color] === 'object')
|
||||||
.slice(0, 17)
|
.slice(0, 17)
|
||||||
@@ -103,8 +103,8 @@ const colors = Object.keys(theme.colors)
|
|||||||
|
|
||||||
export const AllColors: Story = {
|
export const AllColors: Story = {
|
||||||
render: props => (
|
render: props => (
|
||||||
<div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'>
|
<div className='grid grid-flow-col grid-cols-2 grid-rows-9 max-w-2xl w-90vw gap-x-4 gap-y-2'>
|
||||||
{colors.map((color, i) => (
|
{test_colors.map((color, i) => (
|
||||||
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
|
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
23
src/stories/components/PopupMain.stories.tsx
Normal file
23
src/stories/components/PopupMain.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import PopupMain from '@views/components/PopupMain';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/PopupMain',
|
||||||
|
component: PopupMain,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof PopupMain>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/ScheduleTotalHoursAndCourses',
|
||||||
|
component: ScheduleTotalHoursAndCourses,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
scheduleName: { control: 'text' },
|
||||||
|
totalHours: { control: 'number' },
|
||||||
|
totalCourses: { control: 'number' }
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ScheduleTotalHoursAndCourses>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
scheduleName: 'SCHEDULE',
|
||||||
|
totalHours: 22,
|
||||||
|
totalCourses: 8
|
||||||
|
},
|
||||||
|
};
|
||||||
36
src/stories/components/ScheduleListItem.stories.tsx
Normal file
36
src/stories/components/ScheduleListItem.stories.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ScheduleListItem from 'src/views/components/common/ScheduleListItem/ScheduleListItem';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Common/ScheduleListItem',
|
||||||
|
component: ScheduleListItem,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
active: { control: 'boolean' },
|
||||||
|
name: { control: 'text' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = (args) => <ScheduleListItem {...args} />;
|
||||||
|
|
||||||
|
Default.args = {
|
||||||
|
name: 'My Schedule',
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Active = (args) => <ScheduleListItem {...args} />;
|
||||||
|
|
||||||
|
Active.args = {
|
||||||
|
name: 'My Schedule',
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Inactive = (args) => <ScheduleListItem {...args} />;
|
||||||
|
|
||||||
|
Inactive.args = {
|
||||||
|
name: 'My Schedule',
|
||||||
|
active: false,
|
||||||
|
};
|
||||||
19
src/stories/components/Settings.stories.tsx
Normal file
19
src/stories/components/Settings.stories.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Settings from 'src/views/components/Settings';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/Settings',
|
||||||
|
component: Settings,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<typeof Settings>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
23
src/stories/components/calendar/Calendar.stories.tsx
Normal file
23
src/stories/components/calendar/Calendar.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/Calendar',
|
||||||
|
component: Calendar,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Calendar>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
101
src/stories/components/calendar/CalendarBottomBar.stories.tsx
Normal file
101
src/stories/components/calendar/CalendarBottomBar.stories.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import Instructor from '@shared/types/Instructor';
|
||||||
|
import { CalendarBottomBar } from 'src/views/components/calendar/CalendarBottomBar/CalendarBottomBar';
|
||||||
|
import { getCourseColors } from '../../../shared/util/colors';
|
||||||
|
|
||||||
|
const exampleGovCourse: Course = new Course({
|
||||||
|
courseName: 'Nope',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'GOV',
|
||||||
|
description: ['nah', 'aint typing this', 'corndog'],
|
||||||
|
flags: ['no flag for you >:)'],
|
||||||
|
fullName: 'GOV 312L Something something',
|
||||||
|
instructionMode: 'Online',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Bevo',
|
||||||
|
lastName: 'Barrymore',
|
||||||
|
fullName: 'Bevo Barrymore',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '312L',
|
||||||
|
schedule: {
|
||||||
|
meetings: [],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.OPEN,
|
||||||
|
uniqueId: 12345,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const examplePsyCourse: Course = new Course({
|
||||||
|
courseName: 'Nope Again',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'PSY',
|
||||||
|
description: ['nah', 'aint typing this', 'corndog'],
|
||||||
|
flags: ['no flag for you >:)'],
|
||||||
|
fullName: 'PSY 317L Yada yada',
|
||||||
|
instructionMode: 'Online',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Bevo',
|
||||||
|
lastName: 'Etz',
|
||||||
|
fullName: 'Bevo Etz',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '317L',
|
||||||
|
schedule: {
|
||||||
|
meetings: [],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12346',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.CLOSED,
|
||||||
|
uniqueId: 12346,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/CalendarBottomBar',
|
||||||
|
component: CalendarBottomBar,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<typeof CalendarBottomBar>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
courses: [
|
||||||
|
{
|
||||||
|
colors: getCourseColors('pink', 200),
|
||||||
|
courseDeptAndInstr: `${exampleGovCourse.department} ${exampleGovCourse.number} – ${exampleGovCourse.instructors[0].lastName}`,
|
||||||
|
status: exampleGovCourse.status,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colors: getCourseColors('slate', 500),
|
||||||
|
courseDeptAndInstr: `${examplePsyCourse.department} ${examplePsyCourse.number} – ${examplePsyCourse.instructors[0].lastName}`,
|
||||||
|
status: examplePsyCourse.status,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
render: props => (
|
||||||
|
<div className='outline-red outline w-292.5!'>
|
||||||
|
<CalendarBottomBar {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import React from 'react';
|
import { Course, Status } from '@shared/types/Course';
|
||||||
import { Course, Status } from 'src/shared/types/Course';
|
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
|
||||||
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
import { CourseSchedule } from '@shared/types/CourseSchedule';
|
||||||
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
import Instructor from '@shared/types/Instructor';
|
||||||
import Instructor from 'src/shared/types/Instructor';
|
import CalendarCourse from 'src/views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting';
|
||||||
import CalendarCourseCell from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell';
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/CalendarCourseCell',
|
title: 'Components/Calendar/CalendarCourseMeeting',
|
||||||
component: CalendarCourseCell,
|
component: CalendarCourse,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
@@ -17,13 +16,9 @@ const meta = {
|
|||||||
course: { control: 'object' },
|
course: { control: 'object' },
|
||||||
meetingIdx: { control: 'number' },
|
meetingIdx: { control: 'number' },
|
||||||
color: { control: 'color' },
|
color: { control: 'color' },
|
||||||
|
rightIcon: { control: 'object' },
|
||||||
},
|
},
|
||||||
render: (args: any) => (
|
} satisfies Meta<typeof CalendarCourse>;
|
||||||
<div className="w-45">
|
|
||||||
<CalendarCourseCell {...args} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
} satisfies Meta<typeof CalendarCourseCell>;
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
@@ -37,7 +32,7 @@ export const Default: Story = {
|
|||||||
courseName: "Bevo's Default Course",
|
courseName: "Bevo's Default Course",
|
||||||
department: 'BVO',
|
department: 'BVO',
|
||||||
creditHours: 3,
|
creditHours: 3,
|
||||||
status: Status.WAITLISTED,
|
status: Status.OPEN,
|
||||||
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
url: '',
|
url: '',
|
||||||
114
src/stories/components/calendar/CalendarCourseCell.stories.tsx
Normal file
114
src/stories/components/calendar/CalendarCourseCell.stories.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import { getCourseColors } from '@shared/util/colors';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import CalendarCourseCell from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
||||||
|
import React from 'react';
|
||||||
|
import { exampleCourse } from '../PopupCourseBlock.stories';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/CalendarCourseCell',
|
||||||
|
component: CalendarCourseCell,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
courseDeptAndInstr: { control: { type: 'text' } },
|
||||||
|
className: { control: { type: 'text' } },
|
||||||
|
status: { control: { type: 'select', options: Object.values(Status) } },
|
||||||
|
timeAndLocation: { control: { type: 'text' } },
|
||||||
|
colors: { control: { type: 'object' } },
|
||||||
|
},
|
||||||
|
render: (args: any) => (
|
||||||
|
<div className='w-45'>
|
||||||
|
<CalendarCourseCell {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
args: {
|
||||||
|
courseDeptAndInstr: exampleCourse.department,
|
||||||
|
className: exampleCourse.number,
|
||||||
|
status: exampleCourse.status,
|
||||||
|
timeAndLocation: exampleCourse.schedule.meetings[0].getTimeString({ separator: '-' }),
|
||||||
|
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof CalendarCourseCell>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Variants: Story = {
|
||||||
|
render: props => (
|
||||||
|
<div className='grid grid-cols-2 h-40 max-w-60 w-90vw gap-x-4 gap-y-2'>
|
||||||
|
<CalendarCourseCell
|
||||||
|
{...props}
|
||||||
|
// course={new Course({ ...exampleCourse, status: Status.OPEN })}
|
||||||
|
// Course = new Course({
|
||||||
|
// courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
|
// creditHours: 3,
|
||||||
|
// department: 'C S',
|
||||||
|
// description: [
|
||||||
|
// 'Restricted to computer science majors.',
|
||||||
|
// 'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
|
||||||
|
// 'Computer Science 439 and 439H may not both be counted.',
|
||||||
|
// 'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
|
||||||
|
// 'May be counted toward the Independent Inquiry flag requirement.',
|
||||||
|
// ],
|
||||||
|
// flags: ['Independent Inquiry'],
|
||||||
|
// fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
|
// instructionMode: 'In Person',
|
||||||
|
// instructors: [
|
||||||
|
// new Instructor({
|
||||||
|
// firstName: 'Allison',
|
||||||
|
// lastName: 'Norman',
|
||||||
|
// fullName: 'Allison Norman',
|
||||||
|
// }),
|
||||||
|
// ],
|
||||||
|
// isReserved: false,
|
||||||
|
// number: '439',
|
||||||
|
// schedule: {
|
||||||
|
// meetings: [
|
||||||
|
// new CourseMeeting({
|
||||||
|
// days: ['Tuesday', 'Thursday'],
|
||||||
|
// startTime: 930,
|
||||||
|
// endTime: 1050,
|
||||||
|
// }),
|
||||||
|
// new CourseMeeting({
|
||||||
|
// days: ['Friday'],
|
||||||
|
// startTime: 600,
|
||||||
|
// endTime: 720,
|
||||||
|
// }),
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// semester: {
|
||||||
|
// code: '12345',
|
||||||
|
// season: 'Spring',
|
||||||
|
// year: 2024,
|
||||||
|
// },
|
||||||
|
// status: Status.WAITLISTED,
|
||||||
|
// uniqueId: 67890,
|
||||||
|
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
// });
|
||||||
|
|
||||||
|
colors={getCourseColors('green', 500)}
|
||||||
|
/>
|
||||||
|
<CalendarCourseCell
|
||||||
|
{...props}
|
||||||
|
// course={new Course({ ...exampleCourse, status: Status.CLOSED })}
|
||||||
|
colors={getCourseColors('teal', 400)}
|
||||||
|
/>
|
||||||
|
<CalendarCourseCell
|
||||||
|
{...props}
|
||||||
|
// course={new Course({ ...exampleCourse, status: Status.WAITLISTED })}
|
||||||
|
colors={getCourseColors('indigo', 400)}
|
||||||
|
/>
|
||||||
|
<CalendarCourseCell
|
||||||
|
{...props}
|
||||||
|
// course={new Course({ ...exampleCourse, status: Status.CANCELLED })}
|
||||||
|
colors={getCourseColors('red', 500)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
212
src/stories/components/calendar/CalendarGrid.stories.tsx
Normal file
212
src/stories/components/calendar/CalendarGrid.stories.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import { getCourseColors } from '@shared/util/colors';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
||||||
|
import { Serialized } from 'chrome-extension-toolkit';
|
||||||
|
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
import { UserSchedule } from 'src/shared/types/UserSchedule';
|
||||||
|
import CalendarGrid from 'src/views/components/calendar/CalendarGrid/CalendarGrid';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/CalendarGrid',
|
||||||
|
component: CalendarGrid,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
saturdayClass: { control: 'boolean' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof CalendarGrid>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
const exampleCourse: Course = new Course({
|
||||||
|
uniqueId: 50805,
|
||||||
|
number: '314',
|
||||||
|
fullName: 'C S 314 DATA STRUCTURES',
|
||||||
|
courseName: 'DATA STRUCTURES',
|
||||||
|
department: 'C S',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.OPEN,
|
||||||
|
instructors: [
|
||||||
|
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
|
||||||
|
],
|
||||||
|
isReserved: true,
|
||||||
|
description: [
|
||||||
|
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
|
||||||
|
'Computer Science 314 and 314H may not both be counted.',
|
||||||
|
'BVO 311C and 312H may not both be counted.',
|
||||||
|
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
|
||||||
|
'May be counted toward the Quantitative Reasoning flag requirement.',
|
||||||
|
],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.T, DAY_MAP.TH],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: { building: 'UTC', room: '123' },
|
||||||
|
}),
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.TH],
|
||||||
|
startTime: 570,
|
||||||
|
endTime: 630,
|
||||||
|
location: { building: 'JES', room: '123' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
flags: ['Writing', 'Independent Inquiry'],
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exampleSchedule = new UserSchedule({
|
||||||
|
name: 'Example Schedule',
|
||||||
|
courses: [exampleCourse],
|
||||||
|
hours: 3,
|
||||||
|
} as Serialized<UserSchedule>);
|
||||||
|
|
||||||
|
const testData: CalendarGridCourse[] = [
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 4,
|
||||||
|
startIndex: 10,
|
||||||
|
endIndex: 11,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 1',
|
||||||
|
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
|
||||||
|
status: Status.OPEN,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 2,
|
||||||
|
startIndex: 5,
|
||||||
|
endIndex: 6,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 1',
|
||||||
|
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
|
||||||
|
status: Status.OPEN,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 1,
|
||||||
|
startIndex: 10,
|
||||||
|
endIndex: 12,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 2',
|
||||||
|
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
|
||||||
|
status: Status.CLOSED,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 4,
|
||||||
|
startIndex: 10,
|
||||||
|
endIndex: 11,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 1',
|
||||||
|
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
|
||||||
|
status: Status.OPEN,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 1,
|
||||||
|
startIndex: 10,
|
||||||
|
endIndex: 12,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 2',
|
||||||
|
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
|
||||||
|
status: Status.CLOSED,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 1,
|
||||||
|
startIndex: 10,
|
||||||
|
endIndex: 12,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 3',
|
||||||
|
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
|
||||||
|
status: Status.CLOSED,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 1,
|
||||||
|
startIndex: 10,
|
||||||
|
endIndex: 12,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr: 'Course 4',
|
||||||
|
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
|
||||||
|
status: Status.CLOSED,
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course: exampleCourse,
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
saturdayClass: true,
|
||||||
|
courseCells: testData,
|
||||||
|
},
|
||||||
|
};
|
||||||
19
src/stories/components/calendar/CalendarGridCell.stories.tsx
Normal file
19
src/stories/components/calendar/CalendarGridCell.stories.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Calendar.stories.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import CalendarCell from 'src/views/components/calendar/CalendarGridCell/CalendarGridCell';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/CalendarGridCell',
|
||||||
|
component: CalendarCell,
|
||||||
|
parameters: {
|
||||||
|
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
layout: 'centered',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof CalendarCell>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
17
src/stories/components/calendar/CalendarHeader.stories.tsx
Normal file
17
src/stories/components/calendar/CalendarHeader.stories.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import CalendarHeader from 'src/views/components/calendar/CalendarHeader/CalenderHeader';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/CalendarHeader',
|
||||||
|
component: CalendarHeader,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof CalendarHeader>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
146
src/stories/components/calendar/CalendarSchedules.stories.tsx
Normal file
146
src/stories/components/calendar/CalendarSchedules.stories.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
import { CalendarSchedules } from 'src/views/components/calendar/CalendarSchedules/CalendarSchedules';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Calendar/CalendarSchedules',
|
||||||
|
component: CalendarSchedules,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
dummySchedules: { control: 'object' },
|
||||||
|
dummyActiveIndex: { control: 'number' },
|
||||||
|
|
||||||
|
},
|
||||||
|
render: (args: any) => (
|
||||||
|
<div>
|
||||||
|
<CalendarSchedules {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof CalendarSchedules>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
const schedules = [
|
||||||
|
new UserSchedule({
|
||||||
|
courses: [
|
||||||
|
new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Fall',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
name: 'Main Schedule',
|
||||||
|
hours: 0, // Add the missing 'hours' property
|
||||||
|
}),
|
||||||
|
new UserSchedule({
|
||||||
|
courses: [
|
||||||
|
new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Fall',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
name: 'Backup #3',
|
||||||
|
hours: 0, // Add the missing 'hours' property
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
dummySchedules: schedules,
|
||||||
|
dummyActiveIndex: 0,
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
79
src/stories/injected/CourseCatalogInjectedPopup.stories.ts
Normal file
79
src/stories/injected/CourseCatalogInjectedPopup.stories.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import type { Serialized } from 'chrome-extension-toolkit';
|
||||||
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
import { UserSchedule } from 'src/shared/types/UserSchedule';
|
||||||
|
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
|
||||||
|
const exampleCourse: Course = new Course({
|
||||||
|
uniqueId: 50805,
|
||||||
|
number: '314',
|
||||||
|
fullName: 'C S 314 DATA STRUCTURES',
|
||||||
|
courseName: 'DATA STRUCTURES',
|
||||||
|
department: 'C S',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.OPEN,
|
||||||
|
instructors: [
|
||||||
|
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
|
||||||
|
],
|
||||||
|
isReserved: true,
|
||||||
|
description: [
|
||||||
|
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
|
||||||
|
'Computer Science 314 and 314H may not both be counted.',
|
||||||
|
'BVO 311C and 312H may not both be counted.',
|
||||||
|
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
|
||||||
|
'May be counted toward the Quantitative Reasoning flag requirement.',
|
||||||
|
],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.T, DAY_MAP.TH],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: { building: 'UTC', room: '123' },
|
||||||
|
}),
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.TH],
|
||||||
|
startTime: 570,
|
||||||
|
endTime: 630,
|
||||||
|
location: { building: 'JES', room: '123' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
flags: ['Writing', 'Independent Inquiry'],
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exampleSchedule = new UserSchedule({
|
||||||
|
name: 'Example Schedule',
|
||||||
|
courses: [exampleCourse],
|
||||||
|
hours: 3,
|
||||||
|
} as Serialized<UserSchedule>);
|
||||||
|
|
||||||
|
const meta: Meta<typeof CourseCatalogInjectedPopup> = {
|
||||||
|
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
||||||
|
component: CourseCatalogInjectedPopup,
|
||||||
|
argTypes: {
|
||||||
|
onClose: { action: 'onClose' },
|
||||||
|
activeSchedule: { control: 'object' },
|
||||||
|
course: { control: 'object' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CourseCatalogInjectedPopup>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
activeSchedule: exampleSchedule,
|
||||||
|
course: exampleCourse,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,99 +1,115 @@
|
|||||||
import { Course, Status } from 'src/shared/types/Course';
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
import { UserSchedule } from 'src/shared/types/UserSchedule';
|
import { UserSchedule } from 'src/shared/types/UserSchedule';
|
||||||
import CoursePopup from 'src/views/components/injected/CoursePopup/CoursePopup';
|
import CoursePopup from 'src/views/components/injected/CoursePopupOld/CoursePopup';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
|
||||||
const exampleCourse: Course = new Course({
|
const exampleCourse: Course = new Course({
|
||||||
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
creditHours: 3,
|
creditHours: 3,
|
||||||
department: 'C S',
|
department: 'C S',
|
||||||
description: [
|
description: [
|
||||||
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
|
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
|
||||||
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
|
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
|
||||||
'May be counted toward the Quantitative Reasoning flag requirement.',
|
'May be counted toward the Quantitative Reasoning flag requirement.',
|
||||||
'Designed to accommodate 100 or more students.',
|
'Designed to accommodate 100 or more students.',
|
||||||
'Taught as a Web-based course.',
|
'Taught as a Web-based course.',
|
||||||
],
|
|
||||||
flags: ['Quantitative Reasoning'],
|
|
||||||
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
|
||||||
instructionMode: 'Online',
|
|
||||||
instructors: [],
|
|
||||||
isReserved: false,
|
|
||||||
number: '303E',
|
|
||||||
schedule: {
|
|
||||||
meetings: [
|
|
||||||
new CourseMeeting({
|
|
||||||
days: ['Tuesday', 'Thursday'],
|
|
||||||
endTime: 660,
|
|
||||||
startTime: 570,
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
},
|
flags: ['Quantitative Reasoning'],
|
||||||
semester: {
|
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
code: '12345',
|
instructionMode: 'Online',
|
||||||
season: 'Spring',
|
instructors: [
|
||||||
year: 2024,
|
new Instructor({
|
||||||
},
|
firstName: 'William',
|
||||||
status: Status.CANCELLED,
|
lastName: 'Young',
|
||||||
uniqueId: 12345,
|
middleInitial: 'D',
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
fullName: 'William D Young',
|
||||||
|
}),
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'William',
|
||||||
|
lastName: 'Young',
|
||||||
|
middleInitial: 'D',
|
||||||
|
fullName: 'William D Young',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '303E',
|
||||||
|
schedule: {
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Tuesday', 'Thursday'],
|
||||||
|
endTime: 660,
|
||||||
|
startTime: 570,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.CANCELLED,
|
||||||
|
uniqueId: 12345,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
});
|
});
|
||||||
|
|
||||||
const exampleSchedule: UserSchedule = new UserSchedule({
|
const exampleSchedule: UserSchedule = new UserSchedule({
|
||||||
courses: [exampleCourse],
|
courses: [exampleCourse],
|
||||||
name: 'Example Schedule',
|
name: 'Example Schedule',
|
||||||
|
hours: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Injected/CoursePopup',
|
title: 'Components/Injected/CoursePopup',
|
||||||
component: CoursePopup,
|
component: CoursePopup,
|
||||||
// tags: ['autodocs'],
|
// tags: ['autodocs'],
|
||||||
args: {
|
args: {
|
||||||
course: exampleCourse,
|
course: exampleCourse,
|
||||||
activeSchedule: exampleSchedule,
|
activeSchedule: exampleSchedule,
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
course: {
|
|
||||||
control: {
|
|
||||||
type: 'other',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
activeSchedule: {
|
argTypes: {
|
||||||
control: {
|
course: {
|
||||||
type: 'other',
|
control: {
|
||||||
},
|
type: 'other',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activeSchedule: {
|
||||||
|
control: {
|
||||||
|
type: 'other',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
parameters: {
|
||||||
parameters: {
|
design: {
|
||||||
design: {
|
type: 'figma',
|
||||||
type: 'figma',
|
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=602-1879&mode=design&t=BoS5xBrpSsjgQXqv-11',
|
||||||
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=602-1879&mode=design&t=BoS5xBrpSsjgQXqv-11',
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
} satisfies Meta<typeof CoursePopup>;
|
} satisfies Meta<typeof CoursePopup>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Open: Story = {
|
export const Open: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: new Course({ ...exampleCourse, status: Status.OPEN }),
|
course: new Course({ ...exampleCourse, status: Status.OPEN }),
|
||||||
activeSchedule: new UserSchedule({
|
activeSchedule: new UserSchedule({
|
||||||
courses: [],
|
courses: [],
|
||||||
name: 'Example Schedule',
|
name: 'Example Schedule',
|
||||||
}),
|
hours: 0,
|
||||||
},
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Closed: Story = {
|
export const Closed: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: new Course({ ...exampleCourse, status: Status.CLOSED }),
|
course: new Course({ ...exampleCourse, status: Status.CLOSED }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Cancelled: Story = {
|
export const Cancelled: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: new Course({ ...exampleCourse, status: Status.CANCELLED }),
|
course: new Course({ ...exampleCourse, status: Status.CANCELLED }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { SiteSupport } from '../lib/getSiteSupport';
|
|||||||
import { populateSearchInputs } from '../lib/populateSearchInputs';
|
import { populateSearchInputs } from '../lib/populateSearchInputs';
|
||||||
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
||||||
import AutoLoad from './injected/AutoLoad/AutoLoad';
|
import AutoLoad from './injected/AutoLoad/AutoLoad';
|
||||||
import CoursePopup from './injected/CoursePopup/CoursePopup';
|
import CoursePopup from './injected/CoursePopupOld/CoursePopup';
|
||||||
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
|
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
|
||||||
import TableHead from './injected/TableHead';
|
import TableHead from './injected/TableHead';
|
||||||
import TableRow from './injected/TableRow/TableRow';
|
import TableRow from './injected/TableRow/TableRow';
|
||||||
import TableSubheading from './injected/TableSubheading/TableSubheading';
|
import TableSubheading from './injected/TableSubheading/TableSubheading';
|
||||||
|
import CourseCatalogInjectedPopup from './injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST;
|
support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST;
|
||||||
@@ -79,7 +80,7 @@ export default function CourseCatalogMain({ support }: Props) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{selectedCourse && (
|
{selectedCourse && (
|
||||||
<CoursePopup
|
<CourseCatalogInjectedPopup
|
||||||
course={selectedCourse}
|
course={selectedCourse}
|
||||||
activeSchedule={activeSchedule}
|
activeSchedule={activeSchedule}
|
||||||
onClose={handleClearSelectedCourse}
|
onClose={handleClearSelectedCourse}
|
||||||
|
|||||||
@@ -1,24 +1,157 @@
|
|||||||
import { background } from '@shared/messages';
|
import { StatusIcon } from '@shared/util/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// import { FaCalendarAlt, FaCog, FaRedo } from 'react-icons/fa'; // Added FaRedo for the refresh icon
|
||||||
|
import { Status } from 'src/shared/types/Course';
|
||||||
|
import { test_colors } from 'src/stories/components/PopupCourseBlock.stories';
|
||||||
|
import logoImage from '../../assets/logo.png'; // Adjust the path as necessary
|
||||||
import useSchedules from '../hooks/useSchedules';
|
import useSchedules from '../hooks/useSchedules';
|
||||||
import { Button } from './common/Button/Button';
|
import { openTabFromContentScript } from '../lib/openNewTabFromContentScript';
|
||||||
|
import Divider from './common/Divider/Divider';
|
||||||
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
||||||
|
import List from './common/List/List'; // Ensure this path is correctly pointing to your List component
|
||||||
|
import PopupCourseBlock from './common/PopupCourseBlock/PopupCourseBlock';
|
||||||
|
import Text from './common/Text/Text';
|
||||||
|
import { handleOpenCalendar } from './injected/CourseCatalogInjectedPopup/HeadingAndActions';
|
||||||
|
|
||||||
export default function PopupMain() {
|
export default function PopupMain() {
|
||||||
const [activeSchedule, schedules] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
|
|
||||||
// TODO: Add a button to to switch the active schedule
|
const draggableElements = activeSchedule?.courses.map((course, i) => (
|
||||||
|
<PopupCourseBlock key={course.uniqueId} course={course} colors={test_colors[i]} />
|
||||||
|
));
|
||||||
|
|
||||||
|
const handleOpenOptions = async () => {
|
||||||
|
// Not sure if it's bad practice to export this
|
||||||
|
const url = chrome.runtime.getURL('/src/pages/options/index.html');
|
||||||
|
await openTabFromContentScript(url);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
<Button
|
<div className='mx-auto max-w-sm rounded-lg bg-white p-4 shadow-md'>
|
||||||
onClick={() => {
|
<div className='mb-2 flex items-center justify-between bg-white'>
|
||||||
if (!activeSchedule) return;
|
<div className='flex items-center'>
|
||||||
background.clearCourses({ scheduleName: activeSchedule?.name });
|
<img src={logoImage} alt='Logo' style={{ width: '40px', height: '40px', marginRight: '8px' }} />
|
||||||
}}
|
<div>
|
||||||
>
|
<Text as='div' variant='h1-course' style={{ color: '#bf5700', fontSize: '1.3rem' }}>
|
||||||
Clear Courses
|
UT Registration
|
||||||
</Button>
|
</Text>
|
||||||
|
<Text as='div' variant='h1-course' style={{ color: '#f8971f', fontSize: '1.3rem' }}>
|
||||||
|
Plus
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<button
|
||||||
|
style={{ backgroundColor: '#bf5700', borderRadius: '8px', padding: '8px' }}
|
||||||
|
onClick={handleOpenCalendar}
|
||||||
|
>
|
||||||
|
{/* <FaCalendarAlt color='white' /> */}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
marginLeft: '10px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
onClick={handleOpenOptions}
|
||||||
|
>
|
||||||
|
{/* <FaCog color='#C05621' /> */}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider color='#E2E8F0' type='solid' style={{ margin: '1rem 0' }} />
|
||||||
|
<div
|
||||||
|
className='mb-4 rounded-lg bg-white p-2 text-left shadow-inner'
|
||||||
|
style={{ backgroundColor: 'white', border: '1px solid #FBD38D', borderRadius: '0.5rem' }}
|
||||||
|
>
|
||||||
|
<Text as='div' variant='h2-course' style={{ color: '#DD6B20', fontSize: '1.2rem' }}>
|
||||||
|
MAIN SCHEDULE:
|
||||||
|
</Text>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'start', color: '#333f48' }}>
|
||||||
|
<Text
|
||||||
|
as='div'
|
||||||
|
variant='h1'
|
||||||
|
style={{ fontSize: '1.2rem', fontWeight: 'bold', marginRight: '0.5rem' }}
|
||||||
|
>
|
||||||
|
22 HOURS
|
||||||
|
</Text>
|
||||||
|
<Text as='div' variant='h2-course' style={{ fontSize: '1.2rem' }}>
|
||||||
|
8 Courses
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Integrate the List component here */}
|
||||||
|
{activeSchedule ? (
|
||||||
|
<List
|
||||||
|
draggableElements={draggableElements}
|
||||||
|
itemHeight={100} // Adjust based on your content size
|
||||||
|
listHeight={500} // Adjust based on total height you want for the list
|
||||||
|
listWidth={350} // Adjust based on your layout/design
|
||||||
|
gap={12} // Spacing between items
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className='mt-4 flex justify-between border-t border-gray-200 p-4 text-xs'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#6B7280',
|
||||||
|
padding: '1px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginRight: '3px',
|
||||||
|
marginLeft: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatusIcon status={Status.WAITLISTED} className='h-5 w-5 text-white' />
|
||||||
|
</div>
|
||||||
|
<Text as='span' variant='mini'>
|
||||||
|
WAITLISTED
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#6B7280',
|
||||||
|
padding: '1px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginRight: '3px',
|
||||||
|
marginLeft: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatusIcon status={Status.CLOSED} className='h-5 w-5 text-white' />
|
||||||
|
</div>
|
||||||
|
<Text as='span' variant='mini'>
|
||||||
|
CLOSED
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#6B7280',
|
||||||
|
padding: '1px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginRight: '3px',
|
||||||
|
marginLeft: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatusIcon status={Status.CANCELLED} className='h-5 w-5 text-white' />
|
||||||
|
</div>
|
||||||
|
<Text as='span' variant='mini'>
|
||||||
|
CANCELLED
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-2 text-center text-xs'>
|
||||||
|
<div className='inline-flex items-center justify-center'>
|
||||||
|
<Text as='div' variant='mini'>
|
||||||
|
DATA UPDATED ON: 12:00 AM 02/01/2024
|
||||||
|
</Text>
|
||||||
|
{/* <FaRedo className='ml-2 h-4 w-4 text-gray-600' /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/views/components/Settings.tsx
Normal file
14
src/views/components/Settings.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to hold everything for the settings page
|
||||||
|
* @param props className
|
||||||
|
* @returns The content for the settings page
|
||||||
|
*/
|
||||||
|
export default function Settings({ className }: Props) {
|
||||||
|
return <div className={className}>this will be finished laterrrrrrr</div>;
|
||||||
|
}
|
||||||
40
src/views/components/calendar/Calendar/Calendar.tsx
Normal file
40
src/views/components/calendar/Calendar/Calendar.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CalendarHeader from 'src/views/components/calendar/CalendarHeader/CalenderHeader';
|
||||||
|
import { CalendarBottomBar } from '../CalendarBottomBar/CalendarBottomBar';
|
||||||
|
import { CalendarSchedules } from '../CalendarSchedules/CalendarSchedules';
|
||||||
|
import ImportantLinks from '../ImportantLinks';
|
||||||
|
import CalendarGrid from '../CalendarGrid/CalendarGrid';
|
||||||
|
|
||||||
|
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable chip component that follows the design system of the extension.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function Calendar(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CalendarHeader />
|
||||||
|
<div className='h-screen w-full flex flex-col md:flex-row'>
|
||||||
|
<div className='min-h-[30%] flex flex-col items-start gap-2.5 p-5 pl-7'>
|
||||||
|
<div className='min-h-[30%]'>
|
||||||
|
<CalendarSchedules />
|
||||||
|
</div>
|
||||||
|
<ImportantLinks />
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-grow flex-col gap-4 overflow-hidden'>
|
||||||
|
<div className='flex-grow overflow-auto'>
|
||||||
|
<CalendarGrid />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CalendarBottomBar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Text from '../../common/Text/Text';
|
||||||
|
import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
|
||||||
|
import { Button } from '../../common/Button/Button';
|
||||||
|
import ImageIcon from '~icons/material-symbols/image';
|
||||||
|
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
|
||||||
|
|
||||||
|
type CalendarBottomBarProps = {
|
||||||
|
courses?: CalendarCourseCellProps[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => {
|
||||||
|
if (courses?.length === -1) console.log('foo'); // dumb line to make eslint happy
|
||||||
|
return (
|
||||||
|
<div className='w-full flex py-1.25'>
|
||||||
|
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
|
||||||
|
<Text variant='h4'>Async. and Other:</Text>
|
||||||
|
<div className='h-14 inline-flex gap-2.5'>
|
||||||
|
{courses?.map(course => (
|
||||||
|
<CalendarCourseBlock
|
||||||
|
courseDeptAndInstr={course.courseDeptAndInstr}
|
||||||
|
status={course.status}
|
||||||
|
colors={course.colors}
|
||||||
|
key={course.courseDeptAndInstr}
|
||||||
|
className={clsx(course.className, 'w-35!')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center pl-2.5 pr-7.5'>
|
||||||
|
<Button variant='single' color='ut-black' icon={CalendarMonthIcon}>
|
||||||
|
Save as .CAL
|
||||||
|
</Button>
|
||||||
|
<Button variant='single' color='ut-black' icon={ImageIcon}>
|
||||||
|
Save as .PNG
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.component {
|
||||||
|
display: flex;
|
||||||
|
padding: 7px 7px 9px 7px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
gap: 7px;
|
||||||
|
.course-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
width: 154px;
|
||||||
|
.course {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.time-and-location {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Course } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
|
import styles from './CalendarCourseMeeting.module.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CalendarCourseMeeting component.
|
||||||
|
*/
|
||||||
|
export interface CalendarCourseMeetingProps {
|
||||||
|
/** The Course that the meeting is for. */
|
||||||
|
course: Course;
|
||||||
|
/* index into course meeting array to display */
|
||||||
|
meetingIdx?: number;
|
||||||
|
/** The background color for the course. */
|
||||||
|
color: string;
|
||||||
|
/** The icon to display on the right side of the course. This is optional. */
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `CalendarCourseMeeting` is a functional component that displays a course meeting.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <CalendarCourseMeeting course={course} meeting={meeting} color="red" rightIcon={<Icon />} />
|
||||||
|
*/
|
||||||
|
const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
|
||||||
|
course,
|
||||||
|
meetingIdx,
|
||||||
|
}: CalendarCourseMeetingProps) => {
|
||||||
|
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
||||||
|
return (
|
||||||
|
<div className={styles.component}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles['course-detail']}>
|
||||||
|
<div className={styles.course}>
|
||||||
|
{course.department} {course.number} - {course.instructors[0].lastName}
|
||||||
|
</div>
|
||||||
|
<div className={styles['time-and-location']}>
|
||||||
|
{`${meeting.getTimeString({ separator: '-', capitalize: true })}${
|
||||||
|
meeting.location ? ` - ${meeting.location.building}` : ''
|
||||||
|
}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarCourseMeeting;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Status } from '@shared/types/Course';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import type { CourseColors } from 'src/shared/util/colors';
|
||||||
|
import { pickFontColor } from 'src/shared/util/colors';
|
||||||
|
|
||||||
|
import ClosedIcon from '~icons/material-symbols/lock';
|
||||||
|
import WaitlistIcon from '~icons/material-symbols/timelapse';
|
||||||
|
import CancelledIcon from '~icons/material-symbols/warning';
|
||||||
|
|
||||||
|
import Text from '../../common/Text/Text';
|
||||||
|
|
||||||
|
export interface CalendarCourseCellProps {
|
||||||
|
courseDeptAndInstr: string;
|
||||||
|
timeAndLocation?: string;
|
||||||
|
status: Status;
|
||||||
|
colors: CourseColors;
|
||||||
|
className?: string;
|
||||||
|
popup?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
||||||
|
courseDeptAndInstr,
|
||||||
|
timeAndLocation,
|
||||||
|
status,
|
||||||
|
colors,
|
||||||
|
className,
|
||||||
|
popup,
|
||||||
|
}: CalendarCourseCellProps) => {
|
||||||
|
const [showPopup, setShowPopup] = React.useState(false);
|
||||||
|
let rightIcon: React.ReactNode | null = null;
|
||||||
|
if (status === Status.WAITLISTED) {
|
||||||
|
rightIcon = <WaitlistIcon className='h-5 w-5' />;
|
||||||
|
} else if (status === Status.CLOSED) {
|
||||||
|
rightIcon = <ClosedIcon className='h-5 w-5' />;
|
||||||
|
} else if (status === Status.CANCELLED) {
|
||||||
|
rightIcon = <CancelledIcon className='h-5 w-5' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// popup.onClose = () => setShowPopup(false);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setShowPopup(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// whiteText based on secondaryColor
|
||||||
|
const fontColor = pickFontColor(colors.primaryColor);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'h-full w-full flex justify-center rounded p-2 overflow-x-hidden cursor-pointer',
|
||||||
|
fontColor,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.primaryColor,
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className='flex flex-1 flex-col gap-1'>
|
||||||
|
<Text
|
||||||
|
variant='h1-course'
|
||||||
|
className={clsx('-my-0.8 leading-tight', {
|
||||||
|
truncate: timeAndLocation,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{courseDeptAndInstr}
|
||||||
|
</Text>
|
||||||
|
{timeAndLocation && (
|
||||||
|
<Text variant='h3-course' className='-mb-0.5'>
|
||||||
|
{timeAndLocation}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{rightIcon && (
|
||||||
|
<div
|
||||||
|
className='h-fit flex items-center justify-center justify-self-start rounded p-0.5 text-white'
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.secondaryColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rightIcon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{showPopup ? popup : null}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarCourseCell;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.dayLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 13px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 13px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto repeat(5, 6fr);
|
||||||
|
grid-template-rows: repeat(26, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarRow {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
gap: 5px;
|
||||||
|
color: #bf5700;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14.22px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: normal;
|
||||||
|
margin-top: 20px;
|
||||||
|
border-right: 1px solid #dadce0;
|
||||||
|
border-bottom: 1px solid #dadce0;
|
||||||
|
border-left: 1px solid #dadce0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeAndGrid {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeColumn {
|
||||||
|
display: flex;
|
||||||
|
min-height: 573px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1 0 0;
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeBlock {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.timeLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
max-height: 20px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 17px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
border-radius: var(--border-radius-none, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #1a2024;
|
||||||
|
text-align: left;
|
||||||
|
height: 6.6px;
|
||||||
|
align-self: stretch;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
/* Type scale/small */
|
||||||
|
font-family: 'Roboto Flex';
|
||||||
|
font-size: 14.22px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 6px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonIcon {
|
||||||
|
height: 24px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 30px;
|
||||||
|
width: 1px;
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
height: 75%; /* 75% of the container's height */
|
||||||
|
width: 75%; /* 75% of the container's width */
|
||||||
|
background-color: #000000; /* Color of the dot */
|
||||||
|
border-radius: 50%; /* Rounds the corners into a circle */
|
||||||
|
top: 12.5%; /* Centers the dot vertically */
|
||||||
|
left: 12.5%; /* Centers the dot horizontally */
|
||||||
|
}
|
||||||
226
src/views/components/calendar/CalendarGrid/CalendarGrid.tsx
Normal file
226
src/views/components/calendar/CalendarGrid/CalendarGrid.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
// import html2canvas from 'html2canvas';
|
||||||
|
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
import type { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
|
||||||
|
|
||||||
|
import CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
|
||||||
|
/* import calIcon from 'src/assets/icons/cal.svg';
|
||||||
|
import pngIcon from 'src/assets/icons/png.svg';
|
||||||
|
*/
|
||||||
|
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
|
||||||
|
import styles from './CalendarGrid.module.scss';
|
||||||
|
|
||||||
|
|
||||||
|
/* const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
|
||||||
|
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
|
||||||
|
const grid = [];
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
|
const row = [];
|
||||||
|
let hour = hoursOfDay[i];
|
||||||
|
row.push(
|
||||||
|
<div key={hour} className={styles.timeBlock}>
|
||||||
|
<div className={styles.timeLabelContainer}>
|
||||||
|
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
row.push(Array.from({ length: 5 }, (_, j) => <CalendarCell key={j} />));
|
||||||
|
grid.push(row);
|
||||||
|
} */
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
courseCells?: CalendarGridCourse[];
|
||||||
|
saturdayClass?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
|
||||||
|
const [grid, setGrid] = useState([]);
|
||||||
|
const calendarRef = useRef(null); // Create a ref for the calendar grid
|
||||||
|
|
||||||
|
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
|
||||||
|
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
|
||||||
|
|
||||||
|
/* const saveAsPNG = () => {
|
||||||
|
htmlToImage
|
||||||
|
.toPng(calendarRef.current, {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
style: {
|
||||||
|
background: 'white',
|
||||||
|
marginTop: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
marginRight: '20px',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(dataUrl => {
|
||||||
|
let img = new Image();
|
||||||
|
img.src = dataUrl;
|
||||||
|
fetch(dataUrl)
|
||||||
|
.then(response => response.blob())
|
||||||
|
.then(blob => {
|
||||||
|
const href = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = href;
|
||||||
|
link.download = 'my-schedule.png';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error downloading file:', error));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('oops, something went wrong!', error);
|
||||||
|
});
|
||||||
|
}; */
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newGrid = [];
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
|
const row = [];
|
||||||
|
let hour = hoursOfDay[i];
|
||||||
|
let styleProp = {
|
||||||
|
gridColumn: '1',
|
||||||
|
gridRow: `${2 * i + 2}`,
|
||||||
|
};
|
||||||
|
row.push(
|
||||||
|
<div key={hour} className={styles.timeBlock} style={styleProp}>
|
||||||
|
<div className={styles.timeLabelContainer}>
|
||||||
|
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
for (let k = 0; k < 5; k++) {
|
||||||
|
// let shouldRender = false;
|
||||||
|
styleProp = {
|
||||||
|
gridColumn: `${k + 2}`,
|
||||||
|
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
|
||||||
|
};
|
||||||
|
/* let shouldRenderChild = courseCells[iterator]?.calendarGridPoint &&
|
||||||
|
k === courseCells[iterator].calendarGridPoint.dayIndex && i === courseCells[iterator].calendarGridPoint.startIndex;
|
||||||
|
let childElement = <div className={styles.dot}/>; */
|
||||||
|
/* let completeGridCell = shouldRenderChild ? <CalendarCell key={k} children={childElement}/>
|
||||||
|
: <CalendarCell key={k} />; */
|
||||||
|
row.push(<CalendarCell key={k} styleProp={styleProp} />);
|
||||||
|
}
|
||||||
|
newGrid.push(row);
|
||||||
|
}
|
||||||
|
setGrid(newGrid);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.calendarGrid}>
|
||||||
|
{/* Displaying day labels */}
|
||||||
|
<div className={styles.timeBlock} />
|
||||||
|
{daysOfWeek.map(day => (
|
||||||
|
<div key={day} className={styles.day}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{grid.map((row, rowIndex) => row)}
|
||||||
|
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} /> : null}
|
||||||
|
{/* courseCells.map((block: CalendarGridCourse) => (
|
||||||
|
<div
|
||||||
|
key={`${block}`}
|
||||||
|
style={{
|
||||||
|
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
|
||||||
|
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CalendarCourseCell
|
||||||
|
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
||||||
|
timeAndLocation={block.componentProps.timeAndLocation}
|
||||||
|
status={block.componentProps.status}
|
||||||
|
colors={block.componentProps.colors}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarGrid;
|
||||||
|
|
||||||
|
interface AccountForCourseConflictsProps {
|
||||||
|
courseCells: CalendarGridCourse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsProps): JSX.Element[] {
|
||||||
|
// Groups by dayIndex to identify overlaps
|
||||||
|
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
|
||||||
|
const { dayIndex } = cell.calendarGridPoint;
|
||||||
|
if (!acc[dayIndex]) {
|
||||||
|
acc[dayIndex] = [];
|
||||||
|
}
|
||||||
|
acc[dayIndex].push(cell);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Check for overlaps within each day and adjust gridColumnIndex and totalColumns
|
||||||
|
Object.values(days).forEach((dayCells: CalendarGridCourse[]) => {
|
||||||
|
// Sort by start time to ensure proper columnIndex assignment
|
||||||
|
dayCells.sort((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
|
||||||
|
|
||||||
|
dayCells.forEach((cell, _, arr) => {
|
||||||
|
let columnIndex = 1;
|
||||||
|
cell.totalColumns = 1;
|
||||||
|
// Check for overlaps and adjust columnIndex as needed
|
||||||
|
for (let otherCell of arr) {
|
||||||
|
if (otherCell !== cell) {
|
||||||
|
const isOverlapping =
|
||||||
|
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
|
||||||
|
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
|
||||||
|
if (isOverlapping) {
|
||||||
|
console.log('Found overlapping element');
|
||||||
|
// Adjust columnIndex to not overlap with the otherCell
|
||||||
|
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
|
||||||
|
columnIndex = otherCell.gridColumnStart + 1;
|
||||||
|
console.log(columnIndex);
|
||||||
|
}
|
||||||
|
cell.totalColumns += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell.gridColumnStart = columnIndex;
|
||||||
|
cell.gridColumnEnd = columnIndex + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return courseCells.map(block => (
|
||||||
|
<div
|
||||||
|
key={`${block}`}
|
||||||
|
style={{
|
||||||
|
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
|
||||||
|
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
|
||||||
|
width: `calc(100% / ${block.totalColumns})`,
|
||||||
|
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
|
||||||
|
padding: '0px 10px 4px 0px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CalendarCourseCell
|
||||||
|
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
||||||
|
timeAndLocation={block.componentProps.timeAndLocation}
|
||||||
|
status={block.componentProps.status}
|
||||||
|
colors={block.componentProps.colors}
|
||||||
|
popup={<CourseCatalogInjectedPopup course={block.popupProps.course} activeSchedule={block.popupProps.activeSchedule} onClose={block.popupProps.onClose} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <div className={styles.buttonContainer}>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<button className={styles.calendarButton}>
|
||||||
|
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
|
||||||
|
Save as .CAL
|
||||||
|
</button>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<button onClick={saveAsPNG} className={styles.calendarButton}>
|
||||||
|
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
|
||||||
|
Save as .PNG
|
||||||
|
</button>
|
||||||
|
</div> */
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.calendarCell {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 45px;
|
||||||
|
min-height: 40px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hourLine {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
border-radius: 0px;
|
||||||
|
background: rgba(218, 220, 224, 0.25);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './CalendarGridCell.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
styleProp: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing each 1 hour time block of a calendar
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
function CalendarCell({ styleProp }: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className={styles.calendarCell} style={styleProp}>
|
||||||
|
<div className={styles.hourLine} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarCell;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Status } from '@shared/types/Course';
|
||||||
|
import Divider from '../../common/Divider/Divider';
|
||||||
|
import { Button } from '../../common/Button/Button';
|
||||||
|
import Text from '../../common/Text/Text';
|
||||||
|
import MenuIcon from '~icons/material-symbols/menu';
|
||||||
|
import UndoIcon from '~icons/material-symbols/undo';
|
||||||
|
import RedoIcon from '~icons/material-symbols/redo';
|
||||||
|
import SettingsIcon from '~icons/material-symbols/settings';
|
||||||
|
import ScheduleTotalHoursAndCourses from '../../common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
|
||||||
|
import CourseStatus from '../../common/CourseStatus/CourseStatus';
|
||||||
|
import calIcon from 'src/assets/logo.png';
|
||||||
|
|
||||||
|
const CalendarHeader = () => (
|
||||||
|
<div className='min-h-79px min-w-672px flex px-0 py-15'>
|
||||||
|
<div className='flex flex-row gap-20'>
|
||||||
|
<div className='flex gap-10'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<Button variant='single' icon={MenuIcon} color='ut-gray' />
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<img src={calIcon} className='min-w-[48px] max-w-[48px]' alt='UT Registration Plus Logo' />
|
||||||
|
<div className='flex flex-col whitespace-nowrap'>
|
||||||
|
<Text className='leading-trim text-cap font-roboto text-base text-ut-burntorange font-medium'>
|
||||||
|
UT Registration
|
||||||
|
</Text>
|
||||||
|
<Text className='leading-trim text-cap font-roboto text-base text-ut-orange font-medium'>
|
||||||
|
Plus
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<ScheduleTotalHoursAndCourses scheduleName='SCHEDULE' totalHours={22} totalCourses={8} />
|
||||||
|
DATA UPDATED ON: 12:00 AM 02/01/2024
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row items-center space-x-8'>
|
||||||
|
<div className='flex flex-row space-x-4'>
|
||||||
|
<CourseStatus size='small' status={Status.WAITLISTED} />
|
||||||
|
<CourseStatus size='small' status={Status.CLOSED} />
|
||||||
|
<CourseStatus size='small' status={Status.CANCELLED} />
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row'>
|
||||||
|
<Button variant='single' icon={UndoIcon} color='ut-black' />
|
||||||
|
<Button variant='single' icon={RedoIcon} color='ut-black' />
|
||||||
|
<Button variant='single' icon={SettingsIcon} color='ut-black' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider type='solid' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CalendarHeader;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import AddSchedule from '~icons/material-symbols/add';
|
||||||
|
import List from '../../common/List/List';
|
||||||
|
import ScheduleListItem from '../../common/ScheduleListItem/ScheduleListItem';
|
||||||
|
import Text from '../../common/Text/Text';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
dummySchedules?: UserSchedule[];
|
||||||
|
dummyActiveIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CalendarSchedules(props: Props) {
|
||||||
|
const [activeScheduleIndex, setActiveScheduleIndex] = useState(props.dummyActiveIndex || 0);
|
||||||
|
const [schedules, setSchedules] = useState(props.dummySchedules || []);
|
||||||
|
|
||||||
|
const scheduleComponents = schedules.map((schedule, index) => (
|
||||||
|
<ScheduleListItem active={index === activeScheduleIndex} name={schedule.name} />
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ ...props.style }} className='items-center'>
|
||||||
|
<div className='m0 m-b-2 w-full flex justify-between'>
|
||||||
|
<Text variant='h3'>MY SCHEDULES</Text>
|
||||||
|
<div className='cursor-pointer items-center justify-center btn-transition -ml-1.5 hover:text-zinc-400'>
|
||||||
|
<Text variant='h3'>
|
||||||
|
<AddSchedule />
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={30} listWidth={240} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
src/views/components/calendar/ImportantLinks.tsx
Normal file
65
src/views/components/calendar/ImportantLinks.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Text from '../common/Text/Text';
|
||||||
|
import OutwardArrowIcon from '~icons/material-symbols/arrow-outward';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Important Links" section of the calendar website
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ImportantLinks({ className }: Props) {
|
||||||
|
return (
|
||||||
|
<article className={clsx(className, 'flex flex-col gap-2')}>
|
||||||
|
<Text variant='h3'>Important Links</Text>
|
||||||
|
<a
|
||||||
|
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/'
|
||||||
|
className='flex items-center gap-0.5 text-ut-burntorange'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<Text variant='p'>Spring Course Schedule</Text>
|
||||||
|
<OutwardArrowIcon className='h-3 w-3' />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20236/'
|
||||||
|
className='flex items-center gap-0.5 text-ut-burntorange'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<Text variant='p'>Summer Course Schedule</Text>
|
||||||
|
<OutwardArrowIcon className='h-3 w-3' />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href='https://utdirect.utexas.edu/registrar/ris.WBX'
|
||||||
|
className='flex items-center gap-0.5 text-ut-burntorange'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<Text variant='p'>Registration Info Sheet</Text>
|
||||||
|
<OutwardArrowIcon className='h-3 w-3' />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href='https://utdirect.utexas.edu/registration/chooseSemester.WBX'
|
||||||
|
className='flex items-center gap-0.5 text-ut-burntorange'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<Text variant='p'>Register For Courses</Text>
|
||||||
|
<OutwardArrowIcon className='h-3 w-3' />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href='https://utdirect.utexas.edu/apps/degree/audits/'
|
||||||
|
className='flex items-center gap-0.5 text-ut-burntorange'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<Text variant='p'>Degree Audit</Text>
|
||||||
|
<OutwardArrowIcon className='h-3 w-3' />
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
@use 'sass:color';
|
|
||||||
@use 'src/views/styles/colors.module.scss';
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background-color: #000;
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.1s ease-in-out;
|
|
||||||
font-family: 'Inter';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
opacity: 0.5 !important;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@each $color,
|
|
||||||
$value
|
|
||||||
in (
|
|
||||||
primary: colors.$burnt_orange,
|
|
||||||
secondary: colors.$charcoal,
|
|
||||||
tertiary: colors.$bluebonnet,
|
|
||||||
danger: colors.$speedway_brick,
|
|
||||||
warning: colors.$tangerine,
|
|
||||||
success: colors.$turtle_pond,
|
|
||||||
info: colors.$turquoise
|
|
||||||
)
|
|
||||||
{
|
|
||||||
&.#{$color} {
|
|
||||||
background-color: $value;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: color.adjust($value, $lightness: 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible,
|
|
||||||
&:active {
|
|
||||||
background-color: color.adjust($value, $lightness: -10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background-color: $value !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './Button.module.scss';
|
import type IconComponent from '~icons/material-symbols';
|
||||||
|
import { ThemeColor, getThemeColorHexByName, getThemeColorRgbByName } from '../../../../shared/util/themeColors';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
variant: 'filled' | 'outline' | 'single';
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
type?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'success' | 'info';
|
icon?: typeof IconComponent;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
testId?: string;
|
color: ThemeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,26 +20,53 @@ interface Props {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Button({
|
export function Button({
|
||||||
style,
|
|
||||||
className,
|
className,
|
||||||
type,
|
style,
|
||||||
testId,
|
variant,
|
||||||
children,
|
onClick,
|
||||||
|
icon,
|
||||||
disabled,
|
disabled,
|
||||||
title,
|
title,
|
||||||
onClick,
|
color,
|
||||||
|
children,
|
||||||
}: React.PropsWithChildren<Props>): JSX.Element {
|
}: React.PropsWithChildren<Props>): JSX.Element {
|
||||||
|
const Icon = icon;
|
||||||
|
const isIconOnly = !children && !!icon;
|
||||||
|
const colorHex = getThemeColorHexByName(color);
|
||||||
|
const colorRgb = getThemeColorRgbByName(color)?.join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
style={style}
|
style={
|
||||||
data-testid={testId}
|
{
|
||||||
className={clsx(styles.button, className, styles[type ?? 'primary'], {
|
...style,
|
||||||
[styles.disabled]: disabled,
|
color: colorHex,
|
||||||
})}
|
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity)`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={clsx(
|
||||||
|
'btn',
|
||||||
|
{
|
||||||
|
'text-white! bg-opacity-100 hover:enabled:shadow-md active:enabled:shadow-sm shadow-black/20':
|
||||||
|
variant === 'filled',
|
||||||
|
'bg-opacity-0 border-current hover:enabled:bg-opacity-8 border': variant === 'outline',
|
||||||
|
'bg-opacity-0 border-none hover:enabled:bg-opacity-8': variant === 'single', // settings is the only "single"
|
||||||
|
'px-2 py-1.25': isIconOnly && variant !== 'outline',
|
||||||
|
'px-1.75 py-1.25': isIconOnly && variant === 'outline',
|
||||||
|
'px-3.75': variant === 'outline' && !isIconOnly,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
title={title}
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
onClick={disabled ? undefined : onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
>
|
>
|
||||||
{children}
|
{icon && <Icon className='h-6 w-6' />}
|
||||||
|
{!isIconOnly && (
|
||||||
|
<Text variant='h4' className='translate-y-0.08'>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Course, Status } from 'src/shared/types/Course';
|
|
||||||
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
|
||||||
import ClosedIcon from '~icons/material-symbols/lock';
|
|
||||||
import WaitlistIcon from '~icons/material-symbols/timelapse';
|
|
||||||
import CancelledIcon from '~icons/material-symbols/warning';
|
|
||||||
import Text from '../Text/Text';
|
|
||||||
|
|
||||||
export interface CalendarCourseBlockProps {
|
|
||||||
/** The Course that the meeting is for. */
|
|
||||||
course: Course;
|
|
||||||
/* index into course meeting array to display */
|
|
||||||
meetingIdx?: number;
|
|
||||||
/** The background color for the course. */
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CalendarCourseBlock: React.FC<CalendarCourseBlockProps> = ({ course, meetingIdx }: CalendarCourseBlockProps) => {
|
|
||||||
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
|
||||||
let rightIcon: React.ReactNode | null = null;
|
|
||||||
if (course.status === Status.WAITLISTED) {
|
|
||||||
rightIcon = <WaitlistIcon className='h-5 w-5' />;
|
|
||||||
} else if (course.status === Status.CLOSED) {
|
|
||||||
rightIcon = <ClosedIcon className='h-5 w-5' />;
|
|
||||||
} else if (course.status === Status.CANCELLED) {
|
|
||||||
rightIcon = <CancelledIcon className='h-5 w-5' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex justify-center rounded bg-slate-300 p-2 text-ut-black'>
|
|
||||||
<div className='flex flex-1 flex-col gap-1'>
|
|
||||||
<Text variant='h1-course' className='leading-[75%]!'>
|
|
||||||
{course.department} {course.number} - {course.instructors[0].lastName}
|
|
||||||
</Text>
|
|
||||||
<Text variant='h3-course' className='leading-[75%]!'>
|
|
||||||
{`${meeting.getTimeString({ separator: '–', capitalize: true })}${
|
|
||||||
meeting.location ? ` – ${meeting.location.building}` : ''
|
|
||||||
}`}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
{rightIcon && (
|
|
||||||
<div className='h-fit flex items-center justify-center justify-self-start rounded bg-slate-700 p-0.5 text-white'>
|
|
||||||
{rightIcon}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarCourseBlock;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
@use 'sass:color';
|
|
||||||
@use 'src/views/styles/colors.module.scss';
|
|
||||||
|
|
||||||
.dayLabelContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 13px;
|
|
||||||
min-width: 40px;
|
|
||||||
min-height: 13px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex: 1 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarGrid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
grid-template-rows: repeat(13, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarRow {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar {
|
|
||||||
// display: grid;
|
|
||||||
// grid-template-columns: auto repeat(5, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day {
|
|
||||||
gap: 5px;
|
|
||||||
color: colors.$burnt_orange;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14.22px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeAndGrid {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeColumn {
|
|
||||||
display: flex;
|
|
||||||
min-height: 573px;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: 1 0 0;
|
|
||||||
border-radius: var(--border-radius-none, 0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeBlock {
|
|
||||||
display: flex;
|
|
||||||
width: 50px;
|
|
||||||
height: 40.279px;
|
|
||||||
min-width: 50px;
|
|
||||||
max-width: 50px;
|
|
||||||
min-height: 40px;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
.timeLabelContainer {
|
|
||||||
display: flex;
|
|
||||||
max-height: 20px;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 17px;
|
|
||||||
flex: 1 0 0;
|
|
||||||
align-self: stretch;
|
|
||||||
border-radius: var(--border-radius-none, 0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #1A2024;
|
|
||||||
text-align: left;
|
|
||||||
height: 6.6px;
|
|
||||||
align-self: stretch;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
/* Type scale/small */
|
|
||||||
font-family: "Roboto Flex";
|
|
||||||
font-size: 14.22px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styles from './CalendarGrid.module.scss';
|
|
||||||
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
|
|
||||||
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
|
|
||||||
|
|
||||||
const daysOfWeek = Object.values(DAY_MAP).filter(d => d != "Saturday" && d != "Sunday")
|
|
||||||
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
|
|
||||||
const grid = Array.from({ length: 5 }, () =>
|
|
||||||
Array.from({ length: 13 }, (_, columnIndex) => (
|
|
||||||
<CalendarCell key={columnIndex} />
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
|
||||||
* @param props
|
|
||||||
*/
|
|
||||||
const Calendar: React.FC = (props) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.calendar}>
|
|
||||||
<div className={styles.dayLabelContainer}>
|
|
||||||
{/* Empty cell in the top-left corner */}
|
|
||||||
<div className={styles.day} />
|
|
||||||
{/* Displaying day labels */}
|
|
||||||
{daysOfWeek.map(day => (
|
|
||||||
<div key={day} className={styles.day}>
|
|
||||||
{day}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/* Displaying the rest of the calendar */}
|
|
||||||
<div className={styles.timeAndGrid}>
|
|
||||||
<div className={styles.timeColumn}>
|
|
||||||
{hoursOfDay.map((hour) => (
|
|
||||||
<div key={hour} className={styles.timeBlock}>
|
|
||||||
<div className={styles.timeLabelContainer}>
|
|
||||||
<p>{hour % 12 === 0 ? 12 : hour % 12}:00 {hour < 12 ? 'AM' : 'PM'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={styles.calendarGrid}>
|
|
||||||
{grid.map((row, rowIndex) => (
|
|
||||||
row
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Calendar;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.calendarCell {
|
|
||||||
display: flex;
|
|
||||||
width: 165px;
|
|
||||||
height: 52.231px;
|
|
||||||
min-width: 45px;
|
|
||||||
min-height: 40px;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
border: 1px solid #DADCE0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hourLine {
|
|
||||||
width: 165px;
|
|
||||||
height: 1px;
|
|
||||||
border-radius: var(--border-radius-none, 0px);
|
|
||||||
background: rgba(218, 220, 224, 0.25);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styles from './CalendarGridCell.module.scss';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component representing each 1 hour time block of a calendar
|
|
||||||
* @param props
|
|
||||||
*/
|
|
||||||
const CalendarCell: React.FC = (props) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.calendarCell}>
|
|
||||||
<div className={styles.hourLine}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarCell;
|
|
||||||
38
src/views/components/common/Chip/Chip.tsx
Normal file
38
src/views/components/common/Chip/Chip.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type that represents the flags that a course can have.
|
||||||
|
*/
|
||||||
|
export type Flag = 'WR' | 'QR' | 'GC' | 'CD' | 'E' | 'II';
|
||||||
|
export const flagMap: Record<string, Flag> = {
|
||||||
|
Writing: 'WR',
|
||||||
|
'Quantitative Reasoning': 'QR',
|
||||||
|
'Global Cultures': 'GC',
|
||||||
|
'Cultural Diversity in the United States': 'CD',
|
||||||
|
Ethics: 'E',
|
||||||
|
'Independent Inquiry': 'II',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: Flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable chip component that follows the design system of the extension.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function Chip({ label }: React.PropsWithChildren<Props>): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
as='div'
|
||||||
|
variant='h4'
|
||||||
|
className='min-w-5 inline-flex items-center justify-center gap-2.5 rounded-lg px-1 py-0.5'
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#FFD600',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Course } from 'src/shared/types/Course';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for ConflictWithWarningProps
|
||||||
|
*/
|
||||||
|
export interface ConflictsWithWarningProps {
|
||||||
|
className?: string;
|
||||||
|
conflicts: Course[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ConflictsWithWarning component is used to display a warning message when a course conflicts
|
||||||
|
* with another course as part of the labels and details section
|
||||||
|
*
|
||||||
|
* @param props ConflictsWithWarningProps
|
||||||
|
*/
|
||||||
|
export default function ConflictsWithWarning({ className, conflicts }: ConflictsWithWarningProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
variant='mini'
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5 text-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>Conflicts With:</div>
|
||||||
|
{conflicts.map(course => (
|
||||||
|
<div>
|
||||||
|
{`${course.department} ${course.number} (${course.uniqueId})`}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/views/components/common/Dropdown/Dropdown.tsx
Normal file
110
src/views/components/common/Dropdown/Dropdown.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Disclosure, Transition } from '@headlessui/react';
|
||||||
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import React from 'react';
|
||||||
|
import userScheduleHandler from 'src/pages/background/handler/userScheduleHandler';
|
||||||
|
import DropdownArrowDown from '~icons/material-symbols/arrow-drop-down';
|
||||||
|
import DropdownArrowUp from '~icons/material-symbols/arrow-drop-up';
|
||||||
|
import List from '../List/List';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
// Dummy value solely for storybook
|
||||||
|
dummySchedules?: UserSchedule[];
|
||||||
|
dummyActiveIndex?: number;
|
||||||
|
dummyActiveSchedule?: UserSchedule;
|
||||||
|
scheduleComponents?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
||||||
|
*/
|
||||||
|
export default function Dropdown(props: Props) {
|
||||||
|
// Expand/Hide state for dropdown
|
||||||
|
let [expanded, toggle] = React.useState(false);
|
||||||
|
let [activeScheduleIndex, select] = React.useState(props.dummyActiveIndex);
|
||||||
|
let [activeSchedule, selectFrom] = React.useState(props.dummyActiveSchedule);
|
||||||
|
let [scheduleComponents, setScheduleComponents] = React.useState(props.scheduleComponents);
|
||||||
|
|
||||||
|
const schedules = props.dummySchedules;
|
||||||
|
if (schedules == null) {
|
||||||
|
// if no dummy values passed in
|
||||||
|
// useSchedules hook here
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSwitch = () => {
|
||||||
|
toggle(!expanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
// WIP function to swap schedules. Prefer to use the hook when in production
|
||||||
|
const switchSchedule = (index: number) => {
|
||||||
|
const scheduleToSwitchTo = schedules[index];
|
||||||
|
|
||||||
|
select(index);
|
||||||
|
selectFrom(scheduleToSwitchTo);
|
||||||
|
if (scheduleToSwitchTo != null && scheduleToSwitchTo.name != null) {
|
||||||
|
userScheduleHandler.switchSchedule({
|
||||||
|
data: {
|
||||||
|
scheduleName: scheduleToSwitchTo.name,
|
||||||
|
},
|
||||||
|
sender: null,
|
||||||
|
sendResponse: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ ...props.style, height: expanded && schedules ? `${40 * schedules.length + 54}px` : '72px' }}
|
||||||
|
className='items-left absolute w-72 flex flex-col border'
|
||||||
|
>
|
||||||
|
<Disclosure>
|
||||||
|
<Disclosure.Button>
|
||||||
|
<div className='flex items-center border-none bg-white p-3 text-left'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Text as='div' variant='h4' className='text-ut-burntorange mb-1 w-100%'>
|
||||||
|
MAIN SCHEDULE:
|
||||||
|
</Text>
|
||||||
|
<div>
|
||||||
|
<Text variant='h3' className='text-theme-black leading-[75%]!'>
|
||||||
|
{activeSchedule ? activeSchedule.hours : 0} HOURS
|
||||||
|
</Text>
|
||||||
|
<Text variant='h4' className='ml-2.5 text-ut-black leading-[75%]!'>
|
||||||
|
{activeSchedule ? activeSchedule.courses.length : 0} Courses
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Text className='text-ut-burntorange text-2xl font-normal'>
|
||||||
|
{expanded ? <DropdownArrowDown /> : <DropdownArrowUp />}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
enter='transition duration-100 ease-out'
|
||||||
|
enterFrom='transform scale-95 opacity-0'
|
||||||
|
enterTo='transform scale-100 opacity-100'
|
||||||
|
leave='transition duration-75 ease-out'
|
||||||
|
leaveFrom='transform scale-100 opacity-100'
|
||||||
|
leaveTo='transform scale-95 opacity-0'
|
||||||
|
beforeEnter={() => {
|
||||||
|
toggleSwitch();
|
||||||
|
}}
|
||||||
|
afterLeave={() => {
|
||||||
|
toggleSwitch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<List
|
||||||
|
draggableElements={scheduleComponents}
|
||||||
|
itemHeight={30}
|
||||||
|
listHeight={30}
|
||||||
|
listWidth={240}
|
||||||
|
gap={10}
|
||||||
|
/>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
@import 'src/views/styles/base.module.scss';
|
@import 'src/views/styles/base.module.scss';
|
||||||
|
|
||||||
.extensionRoot {
|
.extensionRoot {
|
||||||
font-family: 'Inter' !important;
|
@apply font-sans;
|
||||||
color: #303030;
|
color: #303030;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
40
src/views/components/common/InfoCard/InfoCard.tsx
Normal file
40
src/views/components/common/InfoCard/InfoCard.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
titleText: string;
|
||||||
|
bodyText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A maybe reusable InfoCard component that follows the design system of the extension.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function InfoCard({
|
||||||
|
titleText,
|
||||||
|
bodyText
|
||||||
|
}: React.PropsWithChildren<Props>): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className = 'w-50 flex flex-col items-start justify-center border rounded p-4'
|
||||||
|
style = {{
|
||||||
|
border: "1px solid #D6D2C4",
|
||||||
|
background: "#FFF" // White
|
||||||
|
}}>
|
||||||
|
<div className="flex flex-col items-start self-stretch gap-1.5">
|
||||||
|
<Text variant = "h4" as = 'span'
|
||||||
|
style = {{
|
||||||
|
color: '#F8971F', // Orange
|
||||||
|
}}>
|
||||||
|
{titleText}
|
||||||
|
</Text>
|
||||||
|
<Text variant = "small" as = 'span'
|
||||||
|
style = {{
|
||||||
|
color: '#333F48', // Black
|
||||||
|
}}>
|
||||||
|
{bodyText}
|
||||||
|
</Text>
|
||||||
|
</ div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ export default function Link(props: PropsWithChildren<Props>) {
|
|||||||
<Text
|
<Text
|
||||||
color='bluebonnet'
|
color='bluebonnet'
|
||||||
{...passedProps}
|
{...passedProps}
|
||||||
span
|
as='span'
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.link,
|
styles.link,
|
||||||
{
|
{
|
||||||
|
|||||||
169
src/views/components/common/List/List.tsx
Normal file
169
src/views/components/common/List/List.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { areEqual } from 'react-window';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the List component.
|
||||||
|
*/
|
||||||
|
export interface ListProps {
|
||||||
|
draggableElements: any[]; // Will later define draggableElements based on what types
|
||||||
|
// of components are draggable.
|
||||||
|
itemHeight: number;
|
||||||
|
listHeight: number;
|
||||||
|
listWidth: number;
|
||||||
|
gap: number; // Impacts the spacing between items in the list
|
||||||
|
}
|
||||||
|
|
||||||
|
function initial(draggableElements: any[] = []) {
|
||||||
|
return draggableElements.map((element, index) => ({
|
||||||
|
id: `id:${index}`,
|
||||||
|
content: element as ReactElement,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorder(list: { id: string; content: ReactElement }[], startIndex: number, endIndex: number) {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyle({ provided, style /* , isDragging, gap */ }) {
|
||||||
|
const combined = {
|
||||||
|
...style,
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
};
|
||||||
|
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({ provided, item, style, isDragging /* , gap */ }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...provided.draggableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
style={getStyle({ provided, style /* , isDragging, gap */ })}
|
||||||
|
className={`item ${isDragging ? 'is-dragging' : ''}`}
|
||||||
|
>
|
||||||
|
{React.cloneElement(item.content, { dragHandleProps: provided.dragHandleProps })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RowProps {
|
||||||
|
data: any; // DraggableElements[]; Need to define DraggableElements interface once those components are ready
|
||||||
|
index: number;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row: React.FC<RowProps> = React.memo(({ data: { items, gap }, index, style }) => {
|
||||||
|
const item = items[index];
|
||||||
|
const adjustedStyle = {
|
||||||
|
...style,
|
||||||
|
height: `calc(${style.height}px - ${gap}px)`, // Reduce the height by gap to accommodate the margin
|
||||||
|
marginBottom: `${gap}px`, // Add gap as bottom margin
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={item.id} index={index} key={item.id}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
{provided => <Item provided={provided} item={item} style={adjustedStyle} gap={gap} />}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}, areEqual);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `List` is a functional component that displays a course meeting.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <List draggableElements={elements} />
|
||||||
|
*/
|
||||||
|
const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight, listWidth, gap = 12 }: ListProps) => {
|
||||||
|
const [items, setItems] = useState(() => initial(draggableElements));
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
result => {
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source.index === result.destination.index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItems = reorder(items, result.source.index, result.destination.index);
|
||||||
|
|
||||||
|
setItems(newItems as { id: string; content: React.ReactElement }[]);
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ overflow: 'hidden', width: listWidth }}>
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable
|
||||||
|
droppableId='droppable'
|
||||||
|
direction='vertical'
|
||||||
|
renderClone={(provided, snapshot, rubric) => {
|
||||||
|
let { style } = provided.draggableProps;
|
||||||
|
const transform = style?.transform;
|
||||||
|
|
||||||
|
if (snapshot.isDragging && transform) {
|
||||||
|
console.log(transform);
|
||||||
|
let [, _x, y] = transform.match(/translate\(([-\d]+)px, ([-\d]+)px\)/) || [];
|
||||||
|
|
||||||
|
style.transform = `translate3d(0px, ${y}px, 0px)`; // Apply constrained y value
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Item
|
||||||
|
provided={provided}
|
||||||
|
isDragging={snapshot.isDragging}
|
||||||
|
item={items[rubric.source.index]}
|
||||||
|
style={{
|
||||||
|
style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{provided => (
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
style={{ width: `${listWidth}px`, marginBottom: `-${gap}px` }}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||||
|
{draggableProvided => (
|
||||||
|
<div
|
||||||
|
ref={draggableProvided.innerRef}
|
||||||
|
{...draggableProvided.draggableProps}
|
||||||
|
style={{
|
||||||
|
...draggableProvided.draggableProps.style,
|
||||||
|
// if last item, don't add margin
|
||||||
|
marginBottom: `${gap}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.cloneElement(item.content, {
|
||||||
|
dragHandleProps: draggableProvided.dragHandleProps,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { PropsWithChildren, useCallback } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import styles from './Popup.module.scss';
|
import styles from './Popup.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -19,8 +21,6 @@ export default function Popup({ onClose, children, className, style, testId, ove
|
|||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const bodyRef = React.useRef<HTMLDivElement>(null);
|
const bodyRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleClickOutside = useCallback(
|
const handleClickOutside = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
if (!bodyRef.current) return;
|
if (!bodyRef.current) return;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { Course, Status } from '@shared/types/Course';
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import { CourseColors, pickFontColor } from '@shared/util/colors';
|
||||||
import { StatusIcon } from '@shared/util/icons';
|
import { StatusIcon } from '@shared/util/icons';
|
||||||
import { CourseColors, getCourseColors, pickFontColor } from '@shared/util/colors';
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
||||||
import Text from '../Text/Text';
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export interface PopupCourseBlockProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
course: Course;
|
course: Course;
|
||||||
colors: CourseColors;
|
colors: CourseColors;
|
||||||
|
dragHandleProps?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,7 +21,12 @@ export interface PopupCourseBlockProps {
|
|||||||
*
|
*
|
||||||
* @param props PopupCourseBlockProps
|
* @param props PopupCourseBlockProps
|
||||||
*/
|
*/
|
||||||
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element {
|
export default function PopupCourseBlock({
|
||||||
|
className,
|
||||||
|
course,
|
||||||
|
colors,
|
||||||
|
dragHandleProps,
|
||||||
|
}: PopupCourseBlockProps): JSX.Element {
|
||||||
// whiteText based on secondaryColor
|
// whiteText based on secondaryColor
|
||||||
const fontColor = pickFontColor(colors.primaryColor);
|
const fontColor = pickFontColor(colors.primaryColor);
|
||||||
|
|
||||||
@@ -36,13 +42,11 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
|
|||||||
backgroundColor: colors.secondaryColor,
|
backgroundColor: colors.secondaryColor,
|
||||||
}}
|
}}
|
||||||
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
||||||
|
{...dragHandleProps}
|
||||||
>
|
>
|
||||||
<DragIndicatorIcon className='h-6 w-6 text-white' />
|
<DragIndicatorIcon className='h-6 w-6 text-white' />
|
||||||
</div>
|
</div>
|
||||||
<Text
|
<Text className={clsx('flex-1 py-3.5 truncate', fontColor)} variant='h1-course'>
|
||||||
className={clsx('flex-1 py-3.5 text-ellipsis whitespace-nowrap overflow-hidden', fontColor)}
|
|
||||||
variant='h1-course'
|
|
||||||
>
|
|
||||||
<span className='px-0.5 font-450'>{course.uniqueId}</span> {course.department} {course.number} –{' '}
|
<span className='px-0.5 font-450'>{course.uniqueId}</span> {course.department} {course.number} –{' '}
|
||||||
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
|
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
active?: boolean;
|
||||||
|
name: string;
|
||||||
|
dragHandleProps?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
||||||
|
*/
|
||||||
|
export default function ScheduleListItem(props: Props) {
|
||||||
|
const { dragHandleProps } = props;
|
||||||
|
console.log(props);
|
||||||
|
return (
|
||||||
|
<div style={{ ...props.style }} className='items-center'>
|
||||||
|
<li className='text-ut-burntorange w-100% flex cursor-pointer items-center self-stretch justify-left'>
|
||||||
|
<div className='group flex justify-center'>
|
||||||
|
<div
|
||||||
|
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
||||||
|
{...dragHandleProps}
|
||||||
|
>
|
||||||
|
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
||||||
|
</div>
|
||||||
|
<div className='inline-flex items-center justify-center gap-1.5'>
|
||||||
|
<div className='h-5.5 w-5.5 flex items-center justify-center border-2px border-current rounded-full btn-transition group-active:scale-95'>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'bg-current h-3 w-3 rounded-full transition tansform scale-100 ease-out-expo duration-250',
|
||||||
|
{
|
||||||
|
'scale-0! opacity-0 ease-in-out! duration-200!': !props.active,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Text variant='p'>{props.name}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for ScheduleTotalHoursAndCourses
|
||||||
|
*/
|
||||||
|
export interface ScheduleTotalHoursAndCoursesProps {
|
||||||
|
scheduleName: string;
|
||||||
|
totalHours: number;
|
||||||
|
totalCourses: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ScheduleTotalHoursAndCourses as per the Labels and Details Figma section
|
||||||
|
*
|
||||||
|
* @param props ScheduleTotalHoursAndCoursesProps
|
||||||
|
*/
|
||||||
|
export default function ScheduleTotalHoursAndCourses({
|
||||||
|
scheduleName,
|
||||||
|
totalHours,
|
||||||
|
totalCourses,
|
||||||
|
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className='min-w-64 flex whitespace-nowrap content-center items-baseline gap-2 uppercase'>
|
||||||
|
<Text className='text-[#BF5700]' variant='h1' as='span'>
|
||||||
|
{`${scheduleName}: `}
|
||||||
|
</Text>
|
||||||
|
<Text variant='h3' as='div' className='flex flex-row items-center gap-2 text-[#1A2024]'>
|
||||||
|
{`${totalHours} HOURS`}
|
||||||
|
<Text variant='h4' as='span' className='text-[#333F48]'>
|
||||||
|
{`${totalCourses} courses`}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Popup from '@views/components/common/Popup/Popup';
|
||||||
|
import React from 'react';
|
||||||
|
import type { Course } from 'src/shared/types/Course';
|
||||||
|
import type { UserSchedule } from 'src/shared/types/UserSchedule';
|
||||||
|
|
||||||
|
|
||||||
|
import Description from './Description';
|
||||||
|
import GradeDistribution from './GradeDistribution';
|
||||||
|
import HeadingAndActions from './HeadingAndActions';
|
||||||
|
|
||||||
|
export interface CourseCatalogInjectedPopupProps {
|
||||||
|
course: Course;
|
||||||
|
activeSchedule?: UserSchedule;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CourseCatalogInjectedPopup: React.FC<CourseCatalogInjectedPopupProps> = ({ course, activeSchedule, onClose }) => (
|
||||||
|
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
|
||||||
|
<Description lines={course.description} />
|
||||||
|
<GradeDistribution course={course} />
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
export default CourseCatalogInjectedPopup;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Text from '../../common/Text/Text';
|
||||||
|
|
||||||
|
interface DescriptionProps {
|
||||||
|
lines: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Description: React.FC<DescriptionProps> = ({ lines }: DescriptionProps) => {
|
||||||
|
const keywords = ['prerequisite', 'restricted'];
|
||||||
|
return (
|
||||||
|
<ul className='my-[5px] space-y-1.5 children:marker:text-ut-burntorange'>
|
||||||
|
{lines.map(line => {
|
||||||
|
const isKeywordPresent = keywords.some(keyword => line.toLowerCase().includes(keyword));
|
||||||
|
return (
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<span className='text-ut-burntorange'>•</span>
|
||||||
|
<li key={line}>
|
||||||
|
<Text variant='p' className={clsx({ 'font-bold text-ut-burntorange': isKeywordPresent })}>
|
||||||
|
{line}
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Description;
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import Spinner from '@views/components/common/Spinner/Spinner';
|
||||||
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import Highcharts from 'highcharts';
|
||||||
|
import HighchartsReact from 'highcharts-react-official';
|
||||||
|
import React from 'react';
|
||||||
|
import type { Course } from 'src/shared/types/Course';
|
||||||
|
import type { Distribution, LetterGrade } from 'src/shared/types/Distribution';
|
||||||
|
import { colors } from 'src/shared/util/themeColors';
|
||||||
|
import {
|
||||||
|
NoDataError,
|
||||||
|
queryAggregateDistribution,
|
||||||
|
querySemesterDistribution,
|
||||||
|
} from 'src/views/lib/database/queryDistribution';
|
||||||
|
|
||||||
|
interface GradeDistributionProps {
|
||||||
|
course: Course;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DataStatus {
|
||||||
|
LOADING = 'LOADING',
|
||||||
|
FOUND = 'FOUND',
|
||||||
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRADE_COLORS: Record<LetterGrade, string> = {
|
||||||
|
A: colors.gradeDistribution.a,
|
||||||
|
'A-': colors.gradeDistribution.aminus,
|
||||||
|
'B+': colors.gradeDistribution.bplus,
|
||||||
|
B: colors.gradeDistribution.b,
|
||||||
|
'B-': colors.gradeDistribution.bminus,
|
||||||
|
'C+': colors.gradeDistribution.cplus,
|
||||||
|
C: colors.gradeDistribution.c,
|
||||||
|
'C-': colors.gradeDistribution.cminus,
|
||||||
|
'D+': colors.gradeDistribution.dplus,
|
||||||
|
D: colors.gradeDistribution.d,
|
||||||
|
'D-': colors.gradeDistribution.dminus,
|
||||||
|
F: colors.gradeDistribution.f,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GradeDistribution: React.FC<GradeDistributionProps> = ({ course }) => {
|
||||||
|
const [semester, setSemester] = React.useState('Aggregate');
|
||||||
|
const [distributions, setDistributions] = React.useState<Record<string, Distribution>>({});
|
||||||
|
const [status, setStatus] = React.useState(DataStatus.LOADING);
|
||||||
|
const ref = React.useRef<HighchartsReact.RefObject>(null);
|
||||||
|
|
||||||
|
const chartData = React.useMemo(() => {
|
||||||
|
if (status === DataStatus.FOUND && distributions[semester]) {
|
||||||
|
return Object.entries(distributions[semester]).map(([grade, count]) => ({
|
||||||
|
y: count,
|
||||||
|
color: GRADE_COLORS[grade as LetterGrade],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [distributions, semester, status]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchInitialData = async () => {
|
||||||
|
try {
|
||||||
|
const [aggregateDist, semesters] = await queryAggregateDistribution(course);
|
||||||
|
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
|
||||||
|
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
|
||||||
|
const semesterDistributions = await Promise.all(semesterPromises);
|
||||||
|
semesters.forEach((semester, i) => {
|
||||||
|
initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
|
||||||
|
});
|
||||||
|
setDistributions(initialDistributions);
|
||||||
|
setStatus(DataStatus.FOUND);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (e instanceof NoDataError) {
|
||||||
|
setStatus(DataStatus.NOT_FOUND);
|
||||||
|
} else {
|
||||||
|
setStatus(DataStatus.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchInitialData();
|
||||||
|
}, [course]);
|
||||||
|
|
||||||
|
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSemester(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartOptions: Highcharts.Options = {
|
||||||
|
title: { text: undefined },
|
||||||
|
subtitle: { text: undefined },
|
||||||
|
legend: { enabled: false },
|
||||||
|
xAxis: {
|
||||||
|
title: { text: 'Grade' },
|
||||||
|
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'],
|
||||||
|
crosshair: true,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
min: 0,
|
||||||
|
title: { text: 'Number of Students' },
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
style: { fontFamily: 'Roboto Flex', fontWeight: '600' },
|
||||||
|
spacingBottom: 25,
|
||||||
|
spacingTop: 25,
|
||||||
|
height: 250,
|
||||||
|
},
|
||||||
|
credits: { enabled: false },
|
||||||
|
accessibility: { enabled: true },
|
||||||
|
tooltip: {
|
||||||
|
headerFormat: '<span style="font-size:small; font-weight:bold">{point.key}</span><table>',
|
||||||
|
pointFormat:
|
||||||
|
'<td style="color:{black};padding:0;font-size:small; font-weight:bold;"><b>{point.y:.0f} Students</b></td>',
|
||||||
|
footerFormat: '</table>',
|
||||||
|
shared: true,
|
||||||
|
useHTML: true,
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
bar: { pointPadding: 0.2, borderWidth: 0 },
|
||||||
|
series: { animation: { duration: 700 } },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'column',
|
||||||
|
name: 'Grades',
|
||||||
|
data: chartData,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='pb-[25px] pt-[12px]'>
|
||||||
|
{status === DataStatus.LOADING && <Spinner />}
|
||||||
|
{status === DataStatus.NOT_FOUND && <Text variant='p'>No grade distribution data found</Text>}
|
||||||
|
{status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
|
||||||
|
{status === DataStatus.FOUND && (
|
||||||
|
<>
|
||||||
|
<div className='w-full flex items-center justify-center gap-[12px]'>
|
||||||
|
<Text variant='p'>Grade distribution for {`${course.department} ${course.number}`}</Text>
|
||||||
|
<select
|
||||||
|
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
|
||||||
|
onChange={handleSelectSemester}
|
||||||
|
>
|
||||||
|
{Object.keys(distributions)
|
||||||
|
.sort((k1, k2) => {
|
||||||
|
if (k1 === 'Aggregate') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (k2 === 'Aggregate') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const [season1, year1] = k1.split(' ');
|
||||||
|
const [, year2] = k2.split(' ');
|
||||||
|
if (year1 !== year2) {
|
||||||
|
return parseInt(year2, 10) - parseInt(year1, 10);
|
||||||
|
}
|
||||||
|
return season1 === 'Fall' ? 1 : -1;
|
||||||
|
})
|
||||||
|
.map(semester => (
|
||||||
|
<option key={semester} value={semester}>
|
||||||
|
{semester}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GradeDistribution;
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { Button } from '@views/components/common/Button/Button';
|
||||||
|
import { Chip, flagMap } from '@views/components/common/Chip/Chip';
|
||||||
|
import Divider from '@views/components/common/Divider/Divider';
|
||||||
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import addCourse from 'src/pages/background/lib/addCourse';
|
||||||
|
import removeCourse from 'src/pages/background/lib/removeCourse';
|
||||||
|
import type { Course } from 'src/shared/types/Course';
|
||||||
|
import type { UserSchedule } from 'src/shared/types/UserSchedule';
|
||||||
|
import { openTabFromContentScript } from 'src/views/lib/openNewTabFromContentScript';
|
||||||
|
|
||||||
|
import Add from '~icons/material-symbols/add';
|
||||||
|
import CalendarMonth from '~icons/material-symbols/calendar-month';
|
||||||
|
import CloseIcon from '~icons/material-symbols/close';
|
||||||
|
import Copy from '~icons/material-symbols/content-copy';
|
||||||
|
import Description from '~icons/material-symbols/description';
|
||||||
|
import Mood from '~icons/material-symbols/mood';
|
||||||
|
import Remove from '~icons/material-symbols/remove';
|
||||||
|
import Reviews from '~icons/material-symbols/reviews';
|
||||||
|
|
||||||
|
interface HeadingAndActionProps {
|
||||||
|
/* The course to display */
|
||||||
|
course: Course;
|
||||||
|
/* The active schedule */
|
||||||
|
activeSchedule: UserSchedule;
|
||||||
|
/* The function to call when the popup should be closed */
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleOpenCalendar = async () => {
|
||||||
|
// Not sure if it's bad practice to export this
|
||||||
|
const url = chrome.runtime.getURL('calendar.html');
|
||||||
|
await openTabFromContentScript(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the heading component for the CoursePopup component.
|
||||||
|
*
|
||||||
|
* @param {HeadingAndActionProps} props - The component props.
|
||||||
|
* @returns {JSX.Element} The rendered component.
|
||||||
|
*/
|
||||||
|
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
|
||||||
|
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
|
||||||
|
const [courseAdded, setCourseAdded] = useState<boolean>(
|
||||||
|
activeSchedule.courses.some(course => course.uniqueId === uniqueId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const instructorString = instructors
|
||||||
|
.map(instructor => {
|
||||||
|
const { firstName, lastName } = instructor;
|
||||||
|
if (firstName === '') return lastName;
|
||||||
|
return `${firstName} ${lastName}`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(uniqueId.toString());
|
||||||
|
};
|
||||||
|
const handleOpenRateMyProf = async () => {
|
||||||
|
const openTabs = instructors.map(instructor => {
|
||||||
|
const { fullName } = instructor;
|
||||||
|
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${fullName}`;
|
||||||
|
return openTabFromContentScript(url);
|
||||||
|
});
|
||||||
|
await Promise.all(openTabs);
|
||||||
|
};
|
||||||
|
const handleOpenCES = async () => {
|
||||||
|
// TODO: does not look up the professor just takes you to the page
|
||||||
|
const cisUrl = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580®l=en-US';
|
||||||
|
await openTabFromContentScript(cisUrl);
|
||||||
|
};
|
||||||
|
const handleOpenPastSyllabi = async () => {
|
||||||
|
// not specific to professor
|
||||||
|
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=${courseName}&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`;
|
||||||
|
await openTabFromContentScript(url);
|
||||||
|
};
|
||||||
|
const handleAddOrRemoveCourse = async () => {
|
||||||
|
if (!courseAdded) {
|
||||||
|
await addCourse(activeSchedule.name, course);
|
||||||
|
} else {
|
||||||
|
await removeCourse(activeSchedule.name, course);
|
||||||
|
}
|
||||||
|
setCourseAdded(!courseAdded);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className='w-full pb-3 pt-6'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div className='flex items-center gap-1'>
|
||||||
|
<Text variant='h1' className='truncate'>
|
||||||
|
{courseName}
|
||||||
|
</Text>
|
||||||
|
<Text variant='h1' className='flex-1 whitespace-nowrap'>
|
||||||
|
{' '}
|
||||||
|
({department} {courseNumber})
|
||||||
|
</Text>
|
||||||
|
<Button color='ut-burntorange' variant='single' icon={Copy} onClick={handleCopy}>
|
||||||
|
{uniqueId}
|
||||||
|
</Button>
|
||||||
|
<button className='bg-transparent p-0 btn' onClick={onClose}>
|
||||||
|
<CloseIcon className='h-7 w-7' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2.5 flex-content-center'>
|
||||||
|
<Text variant='h4' className='inline-flex items-center justify-center'>
|
||||||
|
with {instructorString}
|
||||||
|
</Text>
|
||||||
|
<div className='flex-content-centr flex gap-1'>
|
||||||
|
{flags.map(flag => (
|
||||||
|
<Chip label={flagMap[flag]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
{schedule.meetings.map(meeting => (
|
||||||
|
<Text variant='h4'>
|
||||||
|
{meeting.getDaysString({ format: 'long', separator: 'long' })}{' '}
|
||||||
|
{meeting.getTimeString({ separator: ' to ', capitalize: false })}
|
||||||
|
{meeting.location && (
|
||||||
|
<>
|
||||||
|
{` in `}
|
||||||
|
<Text variant='h4' className='text-ut-burntorange underline'>
|
||||||
|
{meeting.location.building}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
|
||||||
|
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
|
||||||
|
<Divider type='solid' color='ut-offwhite' className='h-7' />
|
||||||
|
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
|
||||||
|
RateMyProf
|
||||||
|
</Button>
|
||||||
|
<Button variant='outline' color='ut-teal' icon={Mood} onClick={handleOpenCES}>
|
||||||
|
CES
|
||||||
|
</Button>
|
||||||
|
<Button variant='outline' color='ut-orange' icon={Description} onClick={handleOpenPastSyllabi}>
|
||||||
|
Past Syllabi
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='filled'
|
||||||
|
color={!courseAdded ? 'ut-green' : 'ut-red'}
|
||||||
|
icon={!courseAdded ? Add : Remove}
|
||||||
|
onClick={handleAddOrRemoveCourse}
|
||||||
|
>
|
||||||
|
{!courseAdded ? 'Add Course' : 'Remove Course'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeadingAndActions;
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { Course } from '@shared/types/Course';
|
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import React from 'react';
|
|
||||||
import Card from '@views/components/common/Card/Card';
|
|
||||||
import Icon from '@views/components/common/Icon/Icon';
|
|
||||||
import Link from '@views/components/common/Link/Link';
|
|
||||||
import Text from '@views/components/common/Text/Text';
|
|
||||||
import CourseButtons from './CourseButtons/CourseButtons';
|
|
||||||
import styles from './CourseHeader.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
course: Course;
|
|
||||||
activeSchedule?: UserSchedule;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component displays the header of the course info popup.
|
|
||||||
* It displays the course name, unique id, instructors, and schedule, all formatted nicely.
|
|
||||||
*/
|
|
||||||
export default function CourseHeader({ course, activeSchedule, onClose }: Props) {
|
|
||||||
const getBuildingUrl = (building?: string): string | undefined => {
|
|
||||||
if (!building) return undefined;
|
|
||||||
return `https://utdirect.utexas.edu/apps/campus/buildings/nlogon/maps/UTM/${building}/`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={styles.header}>
|
|
||||||
<Icon className={styles.close} size='large' name='close' onClick={onClose} />
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Text className={styles.courseName} size='large' weight='bold' color='black'>
|
|
||||||
{course.courseName} ({course.department} {course.number})
|
|
||||||
</Text>
|
|
||||||
<Link
|
|
||||||
url={course.url}
|
|
||||||
className={styles.uniqueId}
|
|
||||||
size='medium'
|
|
||||||
weight='semi_bold'
|
|
||||||
color='burnt_orange'
|
|
||||||
title='View course details on UT Course Schedule'
|
|
||||||
>
|
|
||||||
#{course.uniqueId}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Text size='medium' className={styles.instructors}>
|
|
||||||
{`with ${!course.instructors.length ? 'TBA' : ''}`}
|
|
||||||
{course.instructors.map((instructor, index) => {
|
|
||||||
const name = instructor.toString({
|
|
||||||
format: 'first_last',
|
|
||||||
case: 'capitalize',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = instructor.getDirectoryUrl();
|
|
||||||
const numInstructors = course.instructors.length;
|
|
||||||
const isLast = course.instructors.length > 1 && index === course.instructors.length - 1;
|
|
||||||
return (
|
|
||||||
<span key={name}>
|
|
||||||
{numInstructors > 1 && index === course.instructors.length - 1 ? '& ' : ''}
|
|
||||||
<Link
|
|
||||||
key={name}
|
|
||||||
size='medium'
|
|
||||||
weight='normal'
|
|
||||||
url={url}
|
|
||||||
title="View instructor's directory page"
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Link>
|
|
||||||
{numInstructors > 2 && !isLast ? ', ' : ''}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
{course.schedule.meetings.map(meeting => (
|
|
||||||
<Text size='medium' className={styles.meeting} key={meeting.startTime}>
|
|
||||||
<Text span size='medium' weight='bold' color='black'>
|
|
||||||
{meeting.getDaysString({
|
|
||||||
format: 'long',
|
|
||||||
separator: 'short',
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
{' at '}
|
|
||||||
<Text span size='medium'>
|
|
||||||
{meeting.getTimeString({
|
|
||||||
separator: 'to',
|
|
||||||
capitalize: true,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
{' in '}
|
|
||||||
<Link
|
|
||||||
size='medium'
|
|
||||||
weight='normal'
|
|
||||||
title='View building on UT Map'
|
|
||||||
url={getBuildingUrl(meeting.location?.building)}
|
|
||||||
disabled={!meeting.location?.building}
|
|
||||||
>
|
|
||||||
{meeting.location?.building ?? 'TBA'}
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<CourseButtons course={course} activeSchedule={activeSchedule} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ export default function CourseDescription({ course }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Card className={styles.container}>
|
<Card className={styles.container}>
|
||||||
{status === LoadStatus.ERROR && (
|
{status === LoadStatus.ERROR && (
|
||||||
<Text color='speedway_brick' size='medium' weight='bold' align='center'>
|
<Text color='speedway_brick' /* size='medium' weight='bold' align='center' */>
|
||||||
Please refresh the page and log back in using your UT EID and password
|
Please refresh the page and log back in using your UT EID and password
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -72,7 +72,7 @@ function DescriptionLine({ line }: LineProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text className={className} size='medium'>
|
<Text className={className} /* size='medium' */>
|
||||||
{line}
|
{line}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import { Course } from '@shared/types/Course';
|
import { Course } from '@shared/types/Course';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import React from 'react';
|
|
||||||
import { Button } from '@views/components/common/Button/Button';
|
import { Button } from '@views/components/common/Button/Button';
|
||||||
import Card from '@views/components/common/Card/Card';
|
import Card from '@views/components/common/Card/Card';
|
||||||
import Icon from '@views/components/common/Icon/Icon';
|
import Icon from '@views/components/common/Icon/Icon';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import React from 'react';
|
||||||
import styles from './CourseButtons.module.scss';
|
import styles from './CourseButtons.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -83,47 +83,43 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
|
|||||||
<Button
|
<Button
|
||||||
onClick={openRateMyProfessorURL}
|
onClick={openRateMyProfessorURL}
|
||||||
disabled={!course.instructors.length}
|
disabled={!course.instructors.length}
|
||||||
type='primary'
|
variant='filled'
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
color='ut-black'
|
||||||
title='Search for this professor on RateMyProfessor'
|
title='Search for this professor on RateMyProfessor'
|
||||||
>
|
>
|
||||||
<Text size='medium' weight='regular' color='white'>
|
<Text /* size='medium' weight='regular' */ color='white'>RateMyProf</Text>
|
||||||
RateMyProf
|
|
||||||
</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name='school' size='medium' />
|
<Icon className={styles.icon} color='white' name='school' size='medium' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={openSyllabiURL}
|
onClick={openSyllabiURL}
|
||||||
type='secondary'
|
variant='filled'
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
color='ut-black'
|
||||||
title='Search for syllabi for this course'
|
title='Search for syllabi for this course'
|
||||||
>
|
>
|
||||||
<Text size='medium' weight='regular' color='white'>
|
<Text /* size='medium' weight='regular' */ color='white'>Syllabi</Text>
|
||||||
Syllabi
|
|
||||||
</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name='grading' size='medium' />
|
<Icon className={styles.icon} color='white' name='grading' size='medium' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={openTextbookURL}
|
onClick={openTextbookURL}
|
||||||
type='tertiary'
|
variant='filled'
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
color='ut-black'
|
||||||
title='Search for textbooks for this course'
|
title='Search for textbooks for this course'
|
||||||
>
|
>
|
||||||
<Text size='medium' weight='regular' color='white'>
|
<Text /* size='medium' weight='regular' color='white' */>Textbook</Text>
|
||||||
Textbook
|
|
||||||
</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
|
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!activeSchedule}
|
disabled={!activeSchedule}
|
||||||
onClick={isCourseSaved ? handleRemoveCourse : handleSaveCourse}
|
onClick={isCourseSaved ? handleRemoveCourse : handleSaveCourse}
|
||||||
title={isCourseSaved ? 'Remove this course from your schedule' : 'Add this course to your schedule'}
|
title={isCourseSaved ? 'Remove this course from your schedule' : 'Add this course to your schedule'}
|
||||||
type={isCourseSaved ? 'danger' : 'success'}
|
variant='filled'
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
color='ut-black'
|
||||||
>
|
>
|
||||||
<Text size='medium' weight='regular' color='white'>
|
<Text /* size='medium' weight='regular' color='white' */>{isCourseSaved ? 'Remove' : 'Add'}</Text>
|
||||||
{isCourseSaved ? 'Remove' : 'Add'}
|
|
||||||
</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' />
|
<Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' />
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { Course } from '@shared/types/Course';
|
||||||
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import React from 'react';
|
||||||
|
import Card from '@views/components/common/Card/Card';
|
||||||
|
import Icon from '@views/components/common/Icon/Icon';
|
||||||
|
import Link from '@views/components/common/Link/Link';
|
||||||
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import { Button } from 'src/views/components/common/Button/Button';
|
||||||
|
import CourseButtons from './CourseButtons/CourseButtons';
|
||||||
|
import styles from './CourseHeader.module.scss';
|
||||||
|
import CopyIcon from '~icons/material-symbols/content-copy';
|
||||||
|
import CloseIcon from '~icons/material-symbols/close';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
course: Course;
|
||||||
|
activeSchedule?: UserSchedule;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component displays the header of the course info popup.
|
||||||
|
* It displays the course name, unique id, instructors, and schedule, all formatted nicely.
|
||||||
|
*/
|
||||||
|
export default function CourseHeader({ course, activeSchedule, onClose }: Props) {
|
||||||
|
// const getBuildingUrl = (building?: string): string | undefined => {
|
||||||
|
// if (!building) return undefined;
|
||||||
|
// return `https://utdirect.utexas.edu/apps/campus/buildings/nlogon/maps/UTM/${building}/`;
|
||||||
|
// };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx-6 my-5'>
|
||||||
|
<div className='flex items-center justify-start'>
|
||||||
|
<Text variant='h1' className='shrink truncate'>
|
||||||
|
{course.courseName}
|
||||||
|
</Text>
|
||||||
|
<Text variant='h1' className='ml-1 shrink-0'>
|
||||||
|
{`(${course.department} ${course.number})`}
|
||||||
|
</Text>
|
||||||
|
<div className='ml-auto min-w-fit flex shrink-0 gap-0'>
|
||||||
|
<Button icon={CopyIcon} variant='single' className='mr-1 px-2' color='ut-burntorange'>
|
||||||
|
{course.uniqueId}
|
||||||
|
</Button>
|
||||||
|
<button className='btn bg-transparent p-0'>
|
||||||
|
<CloseIcon className='h-7 w-7' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text variant='p'>
|
||||||
|
with{' '}
|
||||||
|
{course.instructors.map(instructor => (
|
||||||
|
<span className=''>{instructor.lastName}</span>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
// <Card className={styles.header}>
|
||||||
|
// <Icon className={styles.close} /* size='large' */ name='close' onClick={onClose} />
|
||||||
|
// <div className={styles.title}>
|
||||||
|
// <Text className={styles.courseName} /* size='large' weight='bold' color='black' */>
|
||||||
|
// {course.courseName} ({course.department} {course.number}) blahhhhh
|
||||||
|
// </Text>
|
||||||
|
// <Link
|
||||||
|
// url={course.url}
|
||||||
|
// className={styles.uniqueId}
|
||||||
|
// /* size='medium'
|
||||||
|
// weight='semi_bold' */
|
||||||
|
// color='burnt_orange'
|
||||||
|
// title='View course details on UT Course Schedule'
|
||||||
|
// >
|
||||||
|
// #{course.uniqueId}
|
||||||
|
// </Link>
|
||||||
|
// </div>
|
||||||
|
// <Text /* size='medium' className={styles.instructors} */>
|
||||||
|
// {`with ${!course.instructors.length ? 'TBA' : ''}`}
|
||||||
|
// {course.instructors.map((instructor, index) => {
|
||||||
|
// const name = instructor.toString({
|
||||||
|
// format: 'first_last',
|
||||||
|
// case: 'capitalize',
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const url = instructor.getDirectoryUrl();
|
||||||
|
// const numInstructors = course.instructors.length;
|
||||||
|
// const isLast = course.instructors.length > 1 && index === course.instructors.length - 1;
|
||||||
|
// return (
|
||||||
|
// <span key={name}>
|
||||||
|
// {numInstructors > 1 && index === course.instructors.length - 1 ? '& ' : ''}
|
||||||
|
// <Link
|
||||||
|
// key={name}
|
||||||
|
// /* size='medium'
|
||||||
|
// weight='normal' */
|
||||||
|
// url={url}
|
||||||
|
// title="View instructor's directory page"
|
||||||
|
// >
|
||||||
|
// {name}
|
||||||
|
// </Link>
|
||||||
|
// {numInstructors > 2 && !isLast ? ', ' : ''}
|
||||||
|
// </span>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </Text>
|
||||||
|
// {course.schedule.meetings.map(meeting => (
|
||||||
|
// <Text /* size='medium' */ className={styles.meeting} key={meeting.startTime}>
|
||||||
|
// <Text as='span' /* size='medium' weight='bold' */ color='black'>
|
||||||
|
// {meeting.getDaysString({
|
||||||
|
// format: 'long',
|
||||||
|
// separator: 'short',
|
||||||
|
// })}
|
||||||
|
// </Text>
|
||||||
|
// {' at '}
|
||||||
|
// <Text as='span' /* size='medium' */>
|
||||||
|
// {meeting.getTimeString({
|
||||||
|
// separator: 'to',
|
||||||
|
// capitalize: true,
|
||||||
|
// })}
|
||||||
|
// </Text>
|
||||||
|
// {' in '}
|
||||||
|
// <Link
|
||||||
|
// /* size='medium'
|
||||||
|
// weight='normal' */
|
||||||
|
// title='View building on UT Map'
|
||||||
|
// url={getBuildingUrl(meeting.location?.building)}
|
||||||
|
// disabled={!meeting.location?.building}
|
||||||
|
// >
|
||||||
|
// {meeting.location?.building ?? 'TBA'}
|
||||||
|
// </Link>
|
||||||
|
// </Text>
|
||||||
|
// ))}
|
||||||
|
|
||||||
|
// <CourseButtons course={course} activeSchedule={activeSchedule} />
|
||||||
|
// </Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Popup from '../../common/Popup/Popup';
|
import Popup from '../../common/Popup/Popup';
|
||||||
import CourseDescription from './CourseDescription/CourseDescription';
|
import CourseDescription from './CourseDescription/CourseDescription';
|
||||||
import CourseHeader from './CourseHeader/CourseHeader';
|
import CourseHeader from './CourseHeader/CourseHeader';
|
||||||
@@ -203,18 +203,18 @@ export default function GradeDistribution({ course }: Props) {
|
|||||||
{status === DataStatus.LOADING && <Spinner />}
|
{status === DataStatus.LOADING && <Spinner />}
|
||||||
{status === DataStatus.ERROR && (
|
{status === DataStatus.ERROR && (
|
||||||
<Card className={styles.text}>
|
<Card className={styles.text}>
|
||||||
<Text color='speedway_brick' size='medium' weight='semi_bold'>
|
<Text color='speedway_brick' /* size='medium' weight='semi_bold' */>
|
||||||
There was an error fetching the grade distribution data
|
There was an error fetching the grade distribution data
|
||||||
</Text>
|
</Text>
|
||||||
<Icon color='speedway_brick' size='large' name='sentiment_dissatisfied' />
|
<Icon color='speedway_brick' /* size='large' */ name='sentiment_dissatisfied' />
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{status === DataStatus.NOT_FOUND && (
|
{status === DataStatus.NOT_FOUND && (
|
||||||
<Card className={styles.text}>
|
<Card className={styles.text}>
|
||||||
<Text color='charcoal' size='medium' weight='semi_bold'>
|
<Text color='charcoal' /* size='medium' weight='semi_bold' */>
|
||||||
No grade distribution data was found for this course
|
No grade distribution data was found for this course
|
||||||
</Text>
|
</Text>
|
||||||
<Icon color='charcoal' size='x_large' name='search_off' />
|
<Icon color='charcoal' /* size='x_large' */ name='search_off' />
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -3,9 +3,9 @@ import { UserSchedule } from '@shared/types/UserSchedule';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Button } from '../../common/Button/Button';
|
import { Button } from '../../common/Button/Button';
|
||||||
import Icon from '../../common/Icon/Icon';
|
|
||||||
import Text from '../../common/Text/Text';
|
|
||||||
import styles from './TableRow.module.scss';
|
import styles from './TableRow.module.scss';
|
||||||
|
import ConflictsWithWarning from '../../common/ConflictsWithWarning/ConflictsWithWarning';
|
||||||
|
import AddIcon from '~icons/material-symbols/add-circle';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
@@ -54,7 +54,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
|||||||
return () => {
|
return () => {
|
||||||
element.classList.remove(styles.inActiveSchedule);
|
element.classList.remove(styles.inActiveSchedule);
|
||||||
};
|
};
|
||||||
}, [activeSchedule, element.classList]);
|
}, [activeSchedule, course, element.classList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSchedule || !course) {
|
if (!activeSchedule || !course) {
|
||||||
@@ -76,7 +76,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
|||||||
element.classList.remove(styles.isConflict);
|
element.classList.remove(styles.isConflict);
|
||||||
setConflicts([]);
|
setConflicts([]);
|
||||||
};
|
};
|
||||||
}, [activeSchedule, course]);
|
}, [activeSchedule, course, element.classList]);
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return null;
|
return null;
|
||||||
@@ -84,20 +84,14 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
|||||||
|
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
<>
|
<>
|
||||||
<Button className={styles.rowButton} onClick={onClick} type='secondary'>
|
<Button
|
||||||
<Icon name='bar_chart' color='white' size='medium' />
|
icon={AddIcon}
|
||||||
</Button>
|
className={styles.rowButton}
|
||||||
{conflicts.length > 0 && (
|
color='ut-burntorange'
|
||||||
<div className={styles.conflictTooltip}>
|
onClick={onClick}
|
||||||
<div className={styles.body}>
|
variant='single'
|
||||||
{conflicts.map(c => (
|
/>
|
||||||
<Text size='small' key={c.uniqueId}>
|
{conflicts.length > 0 && <ConflictsWithWarning className={styles.conflictTooltip} conflicts={conflicts} />}
|
||||||
{c.department} {c.number} ({c.uniqueId})
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>,
|
</>,
|
||||||
container
|
container
|
||||||
);
|
);
|
||||||
|
|||||||
12
src/views/hooks/tests/useFlattenedCourseSchedule.test.ts
Normal file
12
src/views/hooks/tests/useFlattenedCourseSchedule.test.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { convertMinutesToIndex } from '../useFlattenedCourseSchedule';
|
||||||
|
|
||||||
|
describe('useFlattenedCourseSchedule', () => {
|
||||||
|
it('should convert minutes to index correctly', () => {
|
||||||
|
const minutes = 480; // 8:00 AM
|
||||||
|
const expectedIndex = 2; // (480 - 420) / 30 = 2
|
||||||
|
const result = convertMinutesToIndex(minutes);
|
||||||
|
expect(result).toBe(expectedIndex);
|
||||||
|
});
|
||||||
|
});
|
||||||
119
src/views/hooks/useFlattenedCourseSchedule.ts
Normal file
119
src/views/hooks/useFlattenedCourseSchedule.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import type { CalendarCourseCellProps } from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
||||||
|
import type { CourseCatalogInjectedPopupProps } from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
|
||||||
|
import useSchedules from './useSchedules';
|
||||||
|
|
||||||
|
const dayToNumber: { [day: string]: number } = {
|
||||||
|
Monday: 0,
|
||||||
|
Tuesday: 1,
|
||||||
|
Wednesday: 2,
|
||||||
|
Thursday: 3,
|
||||||
|
Friday: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CalendarGridPoint {
|
||||||
|
dayIndex: number;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return type of useFlattenedCourseSchedule
|
||||||
|
*/
|
||||||
|
export interface CalendarGridCourse {
|
||||||
|
calendarGridPoint: CalendarGridPoint;
|
||||||
|
componentProps: CalendarCourseCellProps;
|
||||||
|
gridColumnStart?: number;
|
||||||
|
gridColumnEnd?: number;
|
||||||
|
totalColumns?: number;
|
||||||
|
popupProps: CourseCatalogInjectedPopupProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active schedule, and convert it to be render-able into a calendar.
|
||||||
|
* @returns CalendarGridCourse
|
||||||
|
*/
|
||||||
|
export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
|
||||||
|
const [activeSchedule] = useSchedules();
|
||||||
|
const { courses } = activeSchedule;
|
||||||
|
|
||||||
|
return courses
|
||||||
|
.flatMap(course => {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
department,
|
||||||
|
instructors,
|
||||||
|
schedule: { meetings },
|
||||||
|
} = course;
|
||||||
|
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
|
||||||
|
|
||||||
|
if (meetings.length === 0) {
|
||||||
|
// asynch, online course
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 0,
|
||||||
|
startIndex: 0,
|
||||||
|
endIndex: 0,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr,
|
||||||
|
timeAndLocation: 'Asynchronous',
|
||||||
|
status,
|
||||||
|
colors: {
|
||||||
|
// TODO: figure out colors - these are defaults
|
||||||
|
primaryColor: 'ut-gray',
|
||||||
|
secondaryColor: 'ut-gray',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course,
|
||||||
|
activeSchedule,
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// in-person
|
||||||
|
return meetings.flatMap(meeting => {
|
||||||
|
const { days, startTime, endTime, location } = meeting;
|
||||||
|
const time = meeting.getTimeString({ separator: '-', capitalize: true });
|
||||||
|
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
|
||||||
|
|
||||||
|
return days.map(d => ({
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: dayToNumber[d],
|
||||||
|
startIndex: convertMinutesToIndex(startTime),
|
||||||
|
endIndex: convertMinutesToIndex(endTime),
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr,
|
||||||
|
timeAndLocation,
|
||||||
|
status,
|
||||||
|
colors: {
|
||||||
|
// TODO: figure out colors - these are defaults
|
||||||
|
primaryColor: 'ut-orange',
|
||||||
|
secondaryColor: 'ut-orange',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
course,
|
||||||
|
activeSchedule,
|
||||||
|
onClose: () => {}, // Add onClose property here
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.sort((a: CalendarGridCourse, b: CalendarGridCourse) => {
|
||||||
|
if (a.calendarGridPoint.dayIndex !== b.calendarGridPoint.dayIndex) {
|
||||||
|
return a.calendarGridPoint.dayIndex - b.calendarGridPoint.dayIndex;
|
||||||
|
}
|
||||||
|
if (a.calendarGridPoint.startIndex !== b.calendarGridPoint.startIndex) {
|
||||||
|
return a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex;
|
||||||
|
}
|
||||||
|
return a.calendarGridPoint.endIndex - b.calendarGridPoint.endIndex;
|
||||||
|
});
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user