Compare commits

..

23 Commits

Author SHA1 Message Date
doprz
e4a368fbb6 fix: vitest path alias bug 2024-02-22 23:25:08 -06:00
doprz
29247d5dfa chore: lint-format-docs-tests-bugfixes (#105)
* docs: add jsdoc

* feat: change enums to as const objects

* chore(test): add themeColors.test.ts

* fix: fix tests and bugs with strings.ts util

* fix: path alias imports and tsconfig file bug

* fix: remove --max-warnings 0
2024-02-22 22:42:58 -06:00
Samuel Gunter
bb2efc0b46 feat: updated divider component (#99)
* feat: updated divider component

* refactor: inlined Divider's classes, simplified stories

* fix: style to unocss in story

* style: renamed variant to orientation for buttons

* docs: updated comments in Divider after prop name change
2024-02-22 21:24:41 -06:00
doprz
d5a04c745f feat: Best Practices (#102)
* feat: best practices

* feat: add tests workflow

* feat: add best-practices workflow

* fix: wrong indentation in workflow
2024-02-21 15:54:21 -06:00
doprz
f01cb070b3 feat: Conventional Commits (#103)
* feat: add commitlint and husky hook

* chore: fix indentation
2024-02-21 13:36:25 -06:00
b2eac59ae7 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-20 22:06:52 -06:00
e99664fdae Old icon removed in .tsx 2024-02-20 22:06:48 -06:00
49c0d63f0b Icon added successfully 2024-02-20 22:06:26 -06:00
knownotunknown
8c82282467 Update CalendarGrid.tsx 2024-02-20 21:51:42 -06:00
knownotunknown
0a3c31ec09 Fixed some bugs 2024-02-20 21:39:11 -06:00
knownotunknown
ce7917b474 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-20 21:20:40 -06:00
knownotunknown
a5fe6ec06b Can open tabs, updated injected popup heading. basically done 2024-02-20 21:20:37 -06:00
Samuel Gunter
5a2ee0d19a fix: change Chromatic action to build current branch, not base branch (#100) 2024-02-20 17:20:28 -06:00
knownotunknown
9ec0d106f5 Chrome extension works 2024-02-19 23:03:53 -06:00
knownotunknown
70a3f14e0a Squashed commit of the following:
commit c46e4a51c9
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 21:37:46 2024 -0600

    change from reducer pattern to state variables, remove chartData from state

commit 36bcdd2522
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 21:15:41 2024 -0600

    change grade distribution colors to match updated figma

commit 11a50df88d
Merge: c16b301 b4c96a9
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 17:57:13 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit c16b301ff0
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 17:47:21 2024 -0600

    Kinda complete the handlers

commit 1ac1d9095a
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:36:59 2024 -0600

    Bunch of renaming

commit 925829ad41
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:24:53 2024 -0600

    Fix syllabi url

    Remove unused variable and unnecessary args to url

commit f2e5d51eb3
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:24:22 2024 -0600

    Add TODO

    replace current grade colors with a tailwind palette

commit 747ee44440
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:51 2024 -0600

    Minor tweaks

    change style in header

commit ddfe952a32
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:38 2024 -0600

    Add Grade Distribution Stuff

commit c27bf3c390
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:13 2024 -0600

    Modify story to use proper course info

commit 7afdbac1b8
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 16:37:01 2024 -0600

    description stuff done

commit 1a89432276
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:26:32 2024 -0600

    Rename CoursePopup

    Old one to "Old", remove "2" from new one

commit 4c2b31e61a
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:23:01 2024 -0600

    add todo for calendar button

commit 11b7a51ded
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:22:18 2024 -0600

    add course button onclick handlers

commit f2dfcec838
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 14:52:38 2024 -0600

    some unocss updates

commit f9f375514b
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 13:00:46 2024 -0600

    Add rmp callback

commit 122fc6dbdd
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 13:00:16 2024 -0600

    Change test course to 314

commit 19b124b3bd
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 12:19:21 2024 -0600

    complete CourseHeaderAndActions Component

    added course buttons, using proper subcomponents now.

commit 2eea01fc74
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 11:22:12 2024 -0600

    use chip component in header

commit 9cb13c8fd1
Merge: a62b718 9392085
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 11:21:12 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit a62b718c43
Merge: 43d2675 7b7b858
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 10:57:24 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit 43d2675be5
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 10:54:49 2024 -0600

    some work on course popup

    update the stories and create the header component

commit 31bcef3099
Merge: 874f8d5 fa1d737
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Wed Feb 14 14:33:16 2024 -0600

    Merge branch 'main' into abhinavchadaga/course-catalog-popup

    pulling from main

commit 874f8d56cb
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Wed Feb 14 14:30:24 2024 -0600

    some work
2024-02-19 22:39:26 -06:00
knownotunknown
d69707b8e8 Squashed commit of the following:
commit f6896e37e2
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Mon Feb 19 20:46:57 2024 -0600

    Calendar Page mostly styled

commit a28422e6b0
Merge: 297601e 41e6d77
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Mon Feb 19 18:46:59 2024 -0600

    Merge branch 'hackathon' into Som

commit 297601e715
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sun Feb 18 16:28:29 2024 -0600

    Grid works cleanly with up to two course conflicts. Prob needs refactoring

commit 313a9648c9
Merge: b0a95a6 0acd0b7
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sun Feb 18 15:45:57 2024 -0600

    Merge branch 'hackathon' into Som

commit b0a95a6153
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sun Feb 18 14:10:13 2024 -0600

    Made CourseCells and CalendarGridCells more responsive. CourseCells now rendering onto grid. Still, need to make course cells more responsive, and add edge cases

commit a1a0f00514
Merge: 7479004 ac71b83
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sat Feb 17 17:07:06 2024 -0600

    Merge branch 'hackathon' into Som

commit 7479004a65
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sat Feb 17 16:59:34 2024 -0600

    Need to add sorting
2024-02-19 20:48:00 -06:00
knownotunknown
41e6d77d02 Fixed import error 2024-02-19 18:34:42 -06:00
knownotunknown
11fece0595 Merged in finished CalendarGrid 2024-02-19 18:27:06 -06:00
knownotunknown
c676be4765 Fixed build errors and restructured Calendar page 2024-02-19 18:26:09 -06:00
knownotunknown
b4c96a9a10 Fixed build errors and merging in Casey's branch (driodiwb) 2024-02-19 12:03:23 -06:00
knownotunknown
adba5e1bbc Merge remote-tracking branch 'origin/PopupMain' into hackathon 2024-02-19 11:52:48 -06:00
DhruvArora-03
58dc706ece Merge branch 'feat-conflict-row' into hackathon 2024-02-19 11:29:56 -06:00
vivek12311
ff4ee494b6 Using React-icons. Probably need to take out later 2024-02-17 16:03:28 -06:00
165 changed files with 4951 additions and 3063 deletions

105
.eslintrc
View File

@@ -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,36 +35,33 @@
"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-plusplus": "off",
"no-inner-declarations": "off", "no-inner-declarations": "off",
@@ -83,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",
@@ -110,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": [
@@ -119,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",
@@ -154,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\"])",
@@ -169,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",
@@ -186,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
View 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

View File

@@ -1,6 +1,6 @@
name: "Chromatic" name: 'Chromatic'
on: [push, pull_request_target] on: [push, pull_request]
jobs: jobs:
chromatic: chromatic:
@@ -23,4 +23,4 @@ jobs:
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
View 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
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit $1

View File

@@ -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=

View File

@@ -37,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"
}
} }

123
commitlint.config.ts Normal file
View 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".)',
},
},
},
};

9
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"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",
"react-icons": "^5.0.1",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"sass": "^1.70.0", "sass": "^1.70.0",
"sql.js": "1.10.2", "sql.js": "1.10.2",
@@ -20112,6 +20113,14 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true "dev": true
}, },
"node_modules/react-icons": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz",
"integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@@ -9,14 +9,23 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
"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", "@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",
@@ -28,30 +37,31 @@
"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",
"react-window": "^1.8.10", "sass": "^1.71.1",
"sass": "^1.70.0",
"sql.js": "1.10.2", "sql.js": "1.10.2",
"styled-components": "^6.1.8", "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.13", "@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.13", "@storybook/addon-links": "^7.6.17",
"@storybook/blocks": "^7.6.13", "@storybook/blocks": "^7.6.17",
"@storybook/react": "^7.6.13", "@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.13", "@storybook/react-vite": "^7.6.17",
"@storybook/test": "^7.6.13", "@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.17", "@types/node": "^20.11.19",
"@types/prompts": "^2.4.9", "@types/prompts": "^2.4.9",
"@types/react": "^18.2.55", "@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19", "@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.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
@@ -63,10 +73,12 @@
"@unocss/transformer-directives": "^0.58.5", "@unocss/transformer-directives": "^0.58.5",
"@unocss/transformer-variant-group": "^0.58.5", "@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",
@@ -75,24 +87,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.6", "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.35", "postcss": "^8.4.35",
"prettier": "^3.2.5", "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.13", "storybook": "^7.6.17",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"unocss": "^0.58.5", "unocss": "^0.58.5",
"unplugin-icons": "^0.18.5", "unplugin-icons": "^0.18.5",
"vite": "^5.1.1", "vite": "^5.1.4",
"vite-plugin-inspect": "^0.8.3" "vite-plugin-inspect": "^0.8.3",
"vitest": "^1.3.1"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {

4186
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,4 +1,5 @@
import { defineManifest } from '@crxjs/vite-plugin'; import { defineManifest } from '@crxjs/vite-plugin';
import packageJson from '../package.json'; import packageJson from '../package.json';
// Convert from Semver (example: 0.1.0-beta6) // Convert from Semver (example: 0.1.0-beta6)

View File

@@ -1,5 +1,6 @@
import { BACKGROUND_MESSAGES } from '@shared/messages'; import type { BACKGROUND_MESSAGES } from '@shared/messages';
import { MessageListener } from 'chrome-extension-toolkit'; import { MessageListener } from 'chrome-extension-toolkit';
import onInstall from './events/onInstall'; import onInstall from './events/onInstall';
import onServiceWorkerAlive from './events/onServiceWorkerAlive'; import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate'; import onUpdate from './events/onUpdate';

View File

@@ -1,5 +1,5 @@
import BrowserActionMessages from '@shared/messages/BrowserActionMessages'; import type BrowserActionMessages from '@shared/messages/BrowserActionMessages';
import { MessageHandler } from 'chrome-extension-toolkit'; import type { MessageHandler } from 'chrome-extension-toolkit';
const browserActionHandler: MessageHandler<BrowserActionMessages> = { const browserActionHandler: MessageHandler<BrowserActionMessages> = {
disableBrowserAction({ sender, sendResponse }) { disableBrowserAction({ sender, sendResponse }) {

View File

@@ -1,6 +1,6 @@
import HotReloadingMessages from '@shared/messages/HotReloadingMessages'; import type HotReloadingMessages from '@shared/messages/HotReloadingMessages';
import { DevStore } from '@shared/storage/DevStore'; import { DevStore } from '@shared/storage/DevStore';
import { MessageHandler } from 'chrome-extension-toolkit'; import type { MessageHandler } from 'chrome-extension-toolkit';
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = { const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
async reloadExtension({ sendResponse }) { async reloadExtension({ sendResponse }) {

View File

@@ -1,5 +1,6 @@
import TabManagementMessages from '@shared/messages/TabManagementMessages'; import type TabManagementMessages from '@shared/messages/TabManagementMessages';
import { MessageHandler } from 'chrome-extension-toolkit'; import type { MessageHandler } from 'chrome-extension-toolkit';
import openNewTab from '../util/openNewTab'; import openNewTab from '../util/openNewTab';
const tabManagementHandler: MessageHandler<TabManagementMessages> = { const tabManagementHandler: MessageHandler<TabManagementMessages> = {

View File

@@ -1,6 +1,7 @@
import { UserScheduleMessages } from '@shared/messages/UserScheduleMessages'; import type { UserScheduleMessages } from '@shared/messages/UserScheduleMessages';
import { Course } from '@shared/types/Course'; import { Course } from '@shared/types/Course';
import { MessageHandler } from 'chrome-extension-toolkit'; import type { MessageHandler } from 'chrome-extension-toolkit';
import addCourse from '../lib/addCourse'; import addCourse from '../lib/addCourse';
import clearCourses from '../lib/clearCourses'; import clearCourses from '../lib/clearCourses';
import createSchedule from '../lib/createSchedule'; import createSchedule from '../lib/createSchedule';

View File

@@ -1,5 +1,5 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { Course } from '@shared/types/Course'; import type { Course } from '@shared/types/Course';
/** /**
* *

View File

@@ -1,5 +1,10 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Clears the courses for a given schedule.
* @param scheduleName - The name of the schedule.
* @throws Error if the schedule does not exist.
*/
export default async function clearCourses(scheduleName: string): Promise<void> { export default async function clearCourses(scheduleName: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules'); const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(schedule => schedule.name === scheduleName); const schedule = schedules.find(schedule => schedule.name === scheduleName);

View File

@@ -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);

View File

@@ -1,5 +1,11 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Deletes a schedule with the specified name.
*
* @param scheduleName - The name of the schedule to delete.
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is deleted successfully.
*/
export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> { export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> {
const [schedules, activeIndex] = await Promise.all([ const [schedules, activeIndex] = await Promise.all([
UserScheduleStore.get('schedules'), UserScheduleStore.get('schedules'),

View File

@@ -1,5 +1,5 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { Course } from '@shared/types/Course'; import type { Course } from '@shared/types/Course';
/** /**
* *

View File

@@ -1,5 +1,11 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Renames a schedule with the specified name to a new name.
* @param scheduleName - The name of the schedule to be renamed.
* @param newName - The new name for the schedule.
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is renamed successfully.
*/
export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> { export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules'); const schedules = await UserScheduleStore.get('schedules');
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName); const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);

View File

@@ -1,5 +1,11 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Switches the active schedule to the specified schedule name.
* Throws an error if the schedule does not exist.
* @param scheduleName - The name of the schedule to switch to.
* @returns A Promise that resolves when the active schedule is successfully switched.
*/
export default async function switchSchedule(scheduleName: string): Promise<void> { export default async function switchSchedule(scheduleName: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules'); const schedules = await UserScheduleStore.get('schedules');

View File

@@ -1,5 +1,6 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React from 'react';
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
/** /**
* Calendar page * Calendar page
@@ -8,7 +9,7 @@ import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'
export default function CalendarMain() { export default function CalendarMain() {
return ( return (
<ExtensionRoot> <ExtensionRoot>
<div>Calendar Placeholder</div> <Calendar />
</ExtensionRoot> </ExtensionRoot>
); );
} }

View File

@@ -1,6 +1,5 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -14,5 +13,4 @@
<script src="./index.tsx" type="module"></script> <script src="./index.tsx" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import CalendarMain from './CalendarMain'; import CalendarMain from './CalendarMain';
createRoot(document.getElementById('root')).render(<CalendarMain />); createRoot(document.getElementById('root')).render(<CalendarMain />);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import CourseCatalogMain from '@views/components/CourseCatalogMain'; import CourseCatalogMain from '@views/components/CourseCatalogMain';
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport'; import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
import React from 'react';
import { createRoot } from 'react-dom/client';
const support = getSiteSupport(window.location.href); const support = getSiteSupport(window.location.href);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React from 'react';
/** /**
* *

View File

@@ -1,6 +1,5 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -14,5 +13,4 @@
<script src="./index.tsx" type="module"></script> <script src="./index.tsx" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React from 'react';
/** /**
* *

View File

@@ -1,6 +1,5 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -14,5 +13,4 @@
<script src="./index.tsx" type="module"></script> <script src="./index.tsx" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import App from './App'; import App from './App';
createRoot(document.getElementById('root')).render(<App />); createRoot(document.getElementById('root')).render(<App />);

View File

@@ -1,6 +1,5 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -14,5 +13,4 @@
<script src="./index.tsx" type="module"></script> <script src="./index.tsx" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import PopupMain from '../../views/components/PopupMain'; import PopupMain from '../../views/components/PopupMain';
createRoot(document.getElementById('root')).render(<PopupMain />); createRoot(document.getElementById('root')).render(<PopupMain />);

View File

@@ -1,3 +1,4 @@
/* eslint-disable jsdoc/require-jsdoc */
export default interface BrowserActionMessages { export default interface BrowserActionMessages {
/** make it so that clicking the browser action will open the popup.html */ /** make it so that clicking the browser action will open the popup.html */
enableBrowserAction: () => void; enableBrowserAction: () => void;

View File

@@ -1,3 +1,4 @@
/* eslint-disable jsdoc/require-jsdoc */
export default interface HotReloadingMessages { export default interface HotReloadingMessages {
reloadExtension: () => void; reloadExtension: () => void;
} }

View File

@@ -1,5 +1,8 @@
import { Course } from '../types/Course'; import type { Course } from '@shared/types/Course';
/**
* Represents a collection of user schedule messages.
*/
export interface UserScheduleMessages { export interface UserScheduleMessages {
/** /**
* Add a course to a schedule * Add a course to a schedule

View File

@@ -1,8 +1,9 @@
import { createMessenger } from 'chrome-extension-toolkit'; import { createMessenger } from 'chrome-extension-toolkit';
import BrowserActionMessages from './BrowserActionMessages';
import TabManagementMessages from './TabManagementMessages'; import type BrowserActionMessages from './BrowserActionMessages';
import TAB_MESSAGES from './TabMessages'; import type TabManagementMessages from './TabManagementMessages';
import { UserScheduleMessages } from './UserScheduleMessages'; import type TAB_MESSAGES from './TabMessages';
import type { UserScheduleMessages } from './UserScheduleMessages';
/** /**
* This is a type with all the message definitions that can be sent TO the background script * This is a type with all the message definitions that can be sent TO the background script

View File

@@ -15,6 +15,4 @@ export const ExtensionStore = createLocalStore<IExtensionStore>({
lastUpdate: Date.now(), lastUpdate: Date.now(),
}); });
debugStore({ ExtensionStore }); debugStore({ ExtensionStore });

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
/* eslint-disable max-classes-per-file */ import type { Serialized } from 'chrome-extension-toolkit';
import { Serialized } from 'chrome-extension-toolkit';
import { CourseMeeting } from './CourseMeeting'; import type { CourseMeeting } from './CourseMeeting';
import { CourseSchedule } from './CourseSchedule'; import { CourseSchedule } from './CourseSchedule';
import Instructor from './Instructor'; import Instructor from './Instructor';
@@ -12,12 +12,17 @@ export type InstructionMode = 'Online' | 'In Person' | 'Hybrid';
/** /**
* The status of a course (e.g. open, closed, waitlisted, cancelled) * The status of a course (e.g. open, closed, waitlisted, cancelled)
*/ */
export enum Status { export const Status = {
OPEN = 'OPEN', OPEN: 'OPEN',
CLOSED = 'CLOSED', CLOSED: 'CLOSED',
WAITLISTED = 'WAITLISTED', WAITLISTED: 'WAITLISTED',
CANCELLED = 'CANCELLED', CANCELLED: 'CANCELLED',
} } as const;
/**
* Represents the type of status for a course.
*/
export type StatusType = (typeof Status)[keyof typeof Status];
/** /**
* Represents a semester, with the year and the season for when a course is offered * Represents a semester, with the year and the season for when a course is offered
@@ -49,7 +54,7 @@ export class Course {
/** The number of credits that a course is worth */ /** The number of credits that a course is worth */
creditHours: number; creditHours: number;
/** Is the course open, closed, waitlisted, or cancelled? */ /** Is the course open, closed, waitlisted, or cancelled? */
status: Status; status: StatusType;
/** all the people that are teaching this course, and some metadata about their names */ /** all the people that are teaching this course, and some metadata about their names */
instructors: Instructor[]; instructors: Instructor[];
/** Some courses at UT are reserved for certain groups of people or people within a certain major, which makes it difficult for people outside of that group to register for the course. */ /** Some courses at UT are reserved for certain groups of people or people within a certain major, which makes it difficult for people outside of that group to register for the course. */

View File

@@ -1,4 +1,4 @@
import { Serialized } from 'chrome-extension-toolkit'; import type { Serialized } from 'chrome-extension-toolkit';
/** /**
* a map of the days of the week that a class is taught, and the corresponding abbreviation * a map of the days of the week that a class is taught, and the corresponding abbreviation

View File

@@ -1,5 +1,7 @@
import { Serialized } from 'chrome-extension-toolkit'; import type { Serialized } from 'chrome-extension-toolkit';
import { CourseMeeting, Day, DAY_MAP } from './CourseMeeting';
import type { Day } from './CourseMeeting';
import { CourseMeeting, DAY_MAP } from './CourseMeeting';
/** /**
* This represents the schedule for a course, which includes all the meeting times for the course, as well as helper functions for parsing, serializing, and deserializing the schedule * This represents the schedule for a course, which includes all the meeting times for the course, as well as helper functions for parsing, serializing, and deserializing the schedule

View File

@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */
import { PointOptionsObject } from 'highcharts'; import { PointOptionsObject } from 'highcharts';
import { Semester } from './Course'; import { Semester } from './Course';
/** /**
* Each of the possible letter grades that can be given in a course * Each of the possible letter grades that can be given in a course

View File

@@ -1,4 +1,5 @@
import { Serialized } from 'chrome-extension-toolkit'; import type { Serialized } from 'chrome-extension-toolkit';
import { capitalize } from '../util/string'; import { capitalize } from '../util/string';
/** /**

View File

@@ -1,4 +1,5 @@
import { Serialized } from 'chrome-extension-toolkit'; import type { Serialized } from 'chrome-extension-toolkit';
import { Course } from './Course'; import { Course } from './Course';
/** /**
@@ -7,10 +8,15 @@ 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 {

View File

@@ -1,12 +1,20 @@
import { theme } from 'unocss/preset-mini'; import { theme } from 'unocss/preset-mini';
/**
* Represents the colors for a course.
*/
export interface CourseColors { export interface CourseColors {
primaryColor: string; primaryColor: string;
secondaryColor: string; secondaryColor: string;
} }
// calculates luminance of a hex string /**
function getLuminance(hex: string): number { * Calculates the luminance of a given hexadecimal color.
*
* @param hex - The hexadecimal color value.
* @returns The luminance value between 0 and 1.
*/
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);

View File

@@ -1,15 +1,18 @@
import React, { SVGProps } from 'react'; import type { StatusType } from '@shared/types/Course';
import { Status } from '@shared/types/Course';
import type { SVGProps } from 'react';
import React from 'react';
import ClosedIcon from '~icons/material-symbols/lock'; import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse'; import WaitlistIcon from '~icons/material-symbols/timelapse';
import CancelledIcon from '~icons/material-symbols/warning'; import CancelledIcon from '~icons/material-symbols/warning';
import { Status } from '../types/Course';
/** /**
* Get Icon component based on status * Get Icon component based on status
* @param props.status status * @param props.status status
* @returns React.ReactElement - the icon component * @returns React.ReactElement - the icon component
*/ */
export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: Status }): React.ReactElement { export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: StatusType }): React.ReactElement {
const { status, ...rest } = props; const { status, ...rest } = props;
switch (props.status) { switch (props.status) {

View File

@@ -18,6 +18,8 @@ export function capitalize(input: string): string {
} }
capitalized += ' '; capitalized += ' ';
} }
capitalized = capitalized.trim(); // Remove extra space
return capitalized; return capitalized;
} }
@@ -31,7 +33,7 @@ export function capitalizeFirstLetter(input: string): string {
} }
/** /**
* Cuts the * Cuts the input string to the specified length and adds an ellipsis if the string is longer than the specified length.
* @param input The string to ellipsify. * @param input The string to ellipsify.
* @param length The length of the string to return. * @param length The length of the string to return.
* @returns The ellipsified string. * @returns The ellipsified string.

View 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);
});
});

View 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);
}
});
});

View File

@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { capitalize, capitalizeFirstLetter, ellipsify } 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');
});
});
describe('capitalizeFirstLetter', () => {
it('should return a string with the first letter capitalized', () => {
// Test case 1: Single word
expect(capitalizeFirstLetter('hello')).toBe('Hello');
// Test case 2: Word with all lowercase letters
expect(capitalizeFirstLetter('world')).toBe('World');
// Test case 3: Word with all uppercase letters
expect(capitalizeFirstLetter('EXAMPLE')).toBe('Example');
// Test case 4: Word with mixed case letters
expect(capitalizeFirstLetter('tEsT')).toBe('Test');
});
it('should handle empty string input', () => {
expect(capitalizeFirstLetter('')).toBe('');
});
});
describe('ellipsify', () => {
it('should add ellipsis if the input string exceeds the specified character limit', () => {
// Test case 1: Input string is shorter than the character limit
expect(ellipsify('Hello', 10)).toBe('Hello');
// Test case 2: Input string is equal to the character limit
expect(ellipsify('Hello World', 11)).toBe('Hello World');
// Test case 3: Input string is longer than the character limit
expect(ellipsify('Hello World', 5)).toBe('Hello...');
// Test case 4: Input string is empty
expect(ellipsify('', 5)).toBe('');
});
});

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { getThemeColorHexByName, getThemeColorRgbByName, hexToRgb } from '../themeColors';
describe('hexToRgb', () => {
it('should convert hex color to RGB', () => {
expect(hexToRgb('#BF5700')).toEqual([191, 87, 0]);
expect(hexToRgb('#333F48')).toEqual([51, 63, 72]);
expect(hexToRgb('#f8971f')).toEqual([248, 151, 31]);
expect(hexToRgb('#ffd600')).toEqual([255, 214, 0]);
expect(hexToRgb('#a6cd57')).toEqual([166, 205, 87]);
expect(hexToRgb('#579d42')).toEqual([87, 157, 66]);
expect(hexToRgb('#00a9b7')).toEqual([0, 169, 183]);
expect(hexToRgb('#005f86')).toEqual([0, 95, 134]);
expect(hexToRgb('#9cadb7')).toEqual([156, 173, 183]);
expect(hexToRgb('#d6d2c4')).toEqual([214, 210, 196]);
expect(hexToRgb('#95a5a6')).toEqual([149, 165, 166]);
expect(hexToRgb('#B91C1C')).toEqual([185, 28, 28]);
expect(hexToRgb('#af2e2d')).toEqual([175, 46, 45]);
expect(hexToRgb('#1a2024')).toEqual([26, 32, 36]);
expect(hexToRgb('#22c55e')).toEqual([34, 197, 94]);
expect(hexToRgb('#a3e635')).toEqual([163, 230, 53]);
expect(hexToRgb('#84CC16')).toEqual([132, 204, 22]);
expect(hexToRgb('#FDE047')).toEqual([253, 224, 71]);
expect(hexToRgb('#FACC15')).toEqual([250, 204, 21]);
expect(hexToRgb('#F59E0B')).toEqual([245, 158, 11]);
expect(hexToRgb('#FB923C')).toEqual([251, 146, 60]);
expect(hexToRgb('#F97316')).toEqual([249, 115, 22]);
expect(hexToRgb('#EA580C')).toEqual([234, 88, 12]);
expect(hexToRgb('#DC2626')).toEqual([220, 38, 38]);
expect(hexToRgb('#B91C1C')).toEqual([185, 28, 28]);
});
});
describe('getThemeColorHexByName', () => {
it('should return the hex color value by name', () => {
expect(getThemeColorHexByName('ut-burntorange')).toEqual('#BF5700');
expect(getThemeColorHexByName('ut-offwhite')).toEqual('#D6D2C4');
expect(getThemeColorHexByName('ut-black')).toEqual('#333F48');
// Add more test cases for other theme color names
});
});
describe('getThemeColorRgbByName', () => {
it('should return the RGB color value by name', () => {
expect(getThemeColorRgbByName('ut-burntorange')).toEqual([191, 87, 0]);
expect(getThemeColorRgbByName('ut-offwhite')).toEqual([214, 210, 196]);
expect(getThemeColorRgbByName('ut-black')).toEqual([51, 63, 72]);
// Add more test cases for other theme color names
});
});

View 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);
});
});

View File

@@ -2,21 +2,36 @@ export const colors = {
ut: { ut: {
burntorange: '#BF5700', burntorange: '#BF5700',
black: '#333F48', black: '#333F48',
orange: '#f8971f', orange: '#F8971F',
yellow: '#ffd600', yellow: '#FFD600',
lightgreen: '#a6cd57', lightgreen: '#A6CD57',
green: '#579d42', green: '#579D42',
teal: '#00a9b7', teal: '#00A9B7',
blue: '#005f86', blue: '#005F86',
gray: '#9cadb7', gray: '#9CADB7',
offwhite: '#d6d2c4', offwhite: '#D6D2C4',
concrete: '#95a5a6', 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: { theme: {
red: '#af2e2d', red: '#AF2E2D',
black: '#1a2024', black: '#1A2024',
}, },
} as const; 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 satisfies Record<string, Record<string, string>>;
type NestedKeys<T> = { type NestedKeys<T> = {
[K in keyof T]: T[K] extends Record<string, any> ? `${string & K}-${string & keyof T[K]}` : never; [K in keyof T]: T[K] extends Record<string, any> ? `${string & K}-${string & keyof T[K]}` : never;
@@ -27,6 +42,10 @@ type NestedKeys<T> = {
*/ */
export type ThemeColor = NestedKeys<typeof colors>; export type ThemeColor = NestedKeys<typeof colors>;
/**
* Flattened colors object.
* @type {Record<ThemeColor, string>}
*/
export const colorsFlattened = Object.entries(colors).reduce( export const colorsFlattened = Object.entries(colors).reduce(
(acc, [prefix, group]) => { (acc, [prefix, group]) => {
for (const [name, hex] of Object.entries(group)) { for (const [name, hex] of Object.entries(group)) {
@@ -37,9 +56,18 @@ export const colorsFlattened = Object.entries(colors).reduce(
{} as Record<ThemeColor, string> {} as Record<ThemeColor, string>
); );
const hexToRgb = (hex: string) => /**
* Converts a hexadecimal color code to an RGB color array.
* @param hex The hexadecimal color code to convert.
* @returns An array representing the RGB color values.
*/
export const hexToRgb = (hex: string) =>
hex.match(/[0-9a-f]{2}/gi).map(partialHex => parseInt(partialHex, 16)) as [number, number, number]; hex.match(/[0-9a-f]{2}/gi).map(partialHex => parseInt(partialHex, 16)) as [number, number, number];
/**
* Represents the flattened RGB values of the colors.
* @type {Record<ThemeColor, ReturnType<typeof hexToRgb>>}
*/
const colorsFlattenedRgb = Object.fromEntries( const colorsFlattenedRgb = Object.fromEntries(
Object.entries(colorsFlattened).map(([name, hex]) => [name, hexToRgb(hex)]) Object.entries(colorsFlattened).map(([name, hex]) => [name, hexToRgb(hex)])
) as Record<ThemeColor, ReturnType<typeof hexToRgb>>; ) as Record<ThemeColor, ReturnType<typeof hexToRgb>>;

View File

@@ -5,7 +5,9 @@ export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR; export const DAY = 24 * HOUR;
/** /**
* * Pauses the execution for the specified number of milliseconds.
* @param milliseconds - The number of milliseconds to sleep.
* @returns A promise that resolves after the specified number of milliseconds.
*/ */
export const sleep = (milliseconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, milliseconds)); export const sleep = (milliseconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, milliseconds));

View File

@@ -1,7 +1,8 @@
import { colorsFlattened } from '@shared/util/themeColors';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '@views/components/common/Button/Button';
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 AddIcon from '~icons/material-symbols/add';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
import DescriptionIcon from '~icons/material-symbols/description'; import DescriptionIcon from '~icons/material-symbols/description';
@@ -135,7 +136,7 @@ export const CourseCatalogActionButtons: Story = {
<Button {...props} variant='outline' color='ut-teal' icon={HappyFaceIcon}> <Button {...props} variant='outline' color='ut-teal' icon={HappyFaceIcon}>
CES CES
</Button> </Button>
<Button {...props} variant='outline' color='ut-yellow' icon={DescriptionIcon}> <Button {...props} variant='outline' color='ut-orange' icon={DescriptionIcon}>
Past Syllabi Past Syllabi
</Button> </Button>
<Button {...props} variant='filled' color='ut-green' icon={AddIcon}> <Button {...props} variant='filled' color='ut-green' icon={AddIcon}>

View File

@@ -1,69 +0,0 @@
import { Course, Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import { Meta, StoryObj } from '@storybook/react';
import CalendarCourseCell from '@views/components/common/CalendarCourseCell/CalendarCourseCell';
import React from 'react';
import { exampleCourse } from './PopupCourseBlock.stories';
const meta = {
title: 'Components/Common/CalendarCourseCell',
component: CalendarCourseCell,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
department: { control: { type: 'text' } },
courseNumber: { control: { type: 'text' } },
instructorLastName: { control: { type: 'text' } },
status: { control: { type: 'select', options: Object.values(Status) } },
meetingTime: { control: { type: 'text' } },
colors: { control: { type: 'object' } },
},
render: (args: any) => (
<div className='w-45'>
<CalendarCourseCell {...args} />
</div>
),
args: {
department: exampleCourse.department,
courseNumber: exampleCourse.number,
instructorLastName: exampleCourse.instructors[0].lastName,
status: exampleCourse.status,
meetingTime: 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 })}
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>
),
};

View File

@@ -1,57 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import CalendarGrid from 'src/views/components/common/CalendarGrid/CalendarGrid';
import { getCourseColors } from 'src/shared/util/colors';
import { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
import { Status } from 'src/shared/types/Course';
const meta = {
title: 'Components/Common/CalendarGrid',
component: CalendarGrid,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
saturdayClass: { control: 'boolean' },
},
} satisfies Meta<typeof CalendarGrid>;
export default meta;
const testData: CalendarGridCourse[] = [
{
calendarGridPoint: {
dayIndex: 0,
startIndex: 1,
endIndex: 2,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 2,
endIndex: 3,
},
componentProps: {
courseDeptAndInstr: 'Course 2',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
},
// add more data as needed
];
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
saturdayClass: true,
courseCells: testData,
},
};

View File

@@ -1,19 +0,0 @@
// Calendar.stories.tsx
import React from 'react';
import CalendarCell from '@views/components/common/CalendarGridCell/CalendarGridCell';
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
title: 'Components/Common/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 = {};

View File

@@ -1,5 +1,5 @@
import Card from 'src/views/components/common/Card/Card';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Card from '@views/components/common/Card/Card';
import React from 'react'; import React from 'react';
// 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

View File

@@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { Chip } from 'src/views/components/common/Chip/Chip'; import { Chip } from '@views/components/common/Chip/Chip';
const meta = { const meta = {
title: 'Components/Common/Chip', title: 'Components/Common/Chip',

View File

@@ -1,8 +1,8 @@
import { Meta, StoryObj } from '@storybook/react'; import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor';
import type { Meta, StoryObj } from '@storybook/react';
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning'; 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({ export const ExampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB', courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Status } from '@shared/types/Course'; import { Status } from '@shared/types/Course';
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import CourseStatus from '@views/components/common/CourseStatus/CourseStatus'; import CourseStatus from '@views/components/common/CourseStatus/CourseStatus';
import React from 'react';
const meta = { const meta = {
title: 'Components/Common/CourseStatus', title: 'Components/Common/CourseStatus',
@@ -40,7 +39,7 @@ export const Default: Story = {};
export const Variants: Story = { export const Variants: Story = {
render: args => ( render: args => (
<div className='flex flex-col gap-4 items-center'> <div className='flex flex-col items-center gap-4'>
<CourseStatus {...args} size='small' /> <CourseStatus {...args} size='small' />
<CourseStatus {...args} size='mini' /> <CourseStatus {...args} size='mini' />
</div> </div>

View File

@@ -1,18 +0,0 @@
import Divider from 'src/views/components/common/Divider/Divider';
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
title: 'Components/Common/Divider',
component: Divider,
tags: ['autodocs'],
argTypes: {
color: {
control: 'color',
},
},
} satisfies Meta<typeof Divider>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@@ -0,0 +1,78 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '@views/components/common/Button/Button';
import Divider from '@views/components/common/Divider/Divider';
import React from 'react';
import AddIcon from '~icons/material-symbols/add';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
import DescriptionIcon from '~icons/material-symbols/description';
import HappyFaceIcon from '~icons/material-symbols/mood';
import ReviewsIcon from '~icons/material-symbols/reviews';
const meta = {
title: 'Components/Common/Divider',
component: Divider,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Divider>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Vertical: Story = {
args: {
size: '2.5rem',
orientation: 'vertical',
},
render: props => <Divider {...props} />,
};
export const Horizontal: Story = {
args: {
size: '2.5rem',
orientation: 'horizontal',
},
render: props => <Divider {...props} />,
};
export const IGotHorizontalIGotVerticalWhatYouWant: Story = {
args: {
size: '2.5rem',
orientation: 'vertical',
},
render: props => (
<div className='grid grid-cols-7 grid-rows-3 items-center justify-items-center gap-3.75'>
{Array.from({ length: 21 }).map((_, i) => (
<Divider {...props} orientation={i % 2 === 0 ? 'horizontal' : 'vertical'} />
))}
</div>
),
};
export const CourseCatalogActionButtons: Story = {
args: {
size: '1.75rem',
orientation: 'vertical',
},
render: props => (
<div className='flex items-center gap-3.75'>
<Button variant='filled' color='ut-burntorange' icon={CalendarMonthIcon} />
<Divider {...props} />
<Button variant='outline' color='ut-blue' icon={ReviewsIcon}>
RateMyProf
</Button>
<Button variant='outline' color='ut-teal' icon={HappyFaceIcon}>
CES
</Button>
<Button variant='outline' color='ut-orange' icon={DescriptionIcon}>
Past Syllabi
</Button>
<Button variant='filled' color='ut-green' icon={AddIcon}>
Add Course
</Button>
</div>
),
};

View File

@@ -0,0 +1,157 @@
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import Dropdown from '@views/components/common/Dropdown/Dropdown';
import ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem';
import type { Serialized } from 'chrome-extension-toolkit';
import React from 'react';
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} />
)),
},
};

View File

@@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import ImportantLinks from 'src/views/components/ImportantLinks'; import ImportantLinks from '@views/components/calendar/ImportantLinks';
const meta = { const meta = {
title: 'Components/Common/ImportantLinks', title: 'Components/Common/ImportantLinks',

View File

@@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { InfoCard } from 'src/views/components/common/InfoCard/InfoCard'; import { InfoCard } from '@views/components/common/InfoCard/InfoCard';
const meta = { const meta = {
title: 'Components/Common/InfoCard', title: 'Components/Common/InfoCard',

View File

@@ -1,5 +1,5 @@
import Link from 'src/views/components/common/Link/Link';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Link from '@views/components/common/Link/Link';
const meta = { const meta = {
title: 'Components/Common/Link', title: 'Components/Common/Link',

View File

@@ -1,15 +1,22 @@
import { Course, Status } from '@shared/types/Course'; import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from '@shared/types/CourseMeeting'; import { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor'; import Instructor from '@shared/types/Instructor';
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import List from '@views/components/common/List/List'; import List from '@views/components/common/List/List';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock'; import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import React from 'react'; import React from 'react';
import { test_colors } from './PopupCourseBlock.stories';
import { TestColors } from './PopupCourseBlock.stories';
const numberOfCourses = 5; const numberOfCourses = 5;
const generateCourses = count => { /**
* Generates an array of courses.
*
* @param count - The number of courses to generate.
* @returns An array of generated courses.
*/
export const GenerateCourses = count => {
const courses = []; const courses = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
@@ -61,10 +68,10 @@ const generateCourses = count => {
return courses; return courses;
}; };
const exampleCourses = generateCourses(numberOfCourses); const exampleCourses = GenerateCourses(numberOfCourses);
const generateCourseBlocks = (exampleCourses, colors) => const generateCourseBlocks = (exampleCourses, colors) =>
exampleCourses.map((course, i) => <PopupCourseBlock key={course.uniqueId} course={course} colors={colors[i]} />); exampleCourses.map((course, i) => <PopupCourseBlock key={course.uniqueId} course={course} colors={colors[i]} />);
const exampleCourseBlocks = generateCourseBlocks(exampleCourses, test_colors); export const ExampleCourseBlocks = generateCourseBlocks(exampleCourses, TestColors);
const meta = { const meta = {
title: 'Components/Common/List', title: 'Components/Common/List',
@@ -87,7 +94,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: { args: {
draggableElements: exampleCourseBlocks, draggableElements: ExampleCourseBlocks,
itemHeight: 55, itemHeight: 55,
listHeight: 300, listHeight: 300,
listWidth: 300, listWidth: 300,

View File

@@ -1,13 +1,19 @@
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor';
import { getCourseColors } from '@shared/util/colors';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock'; 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 { CourseMeeting } from 'src/shared/types/CourseMeeting';
import Instructor from 'src/shared/types/Instructor';
import { getCourseColors } from 'src/shared/util/colors';
import { theme } from 'unocss/preset-mini'; import { theme } from 'unocss/preset-mini';
export const exampleCourse: Course = new Course({ /**
* Represents an example course.
*
* @remarks
* This is a sample course object that provides information about a specific 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',
@@ -62,7 +68,7 @@ const meta = {
// More on argTypes: https://storybook.js.org/docs/api/argtypes // More on argTypes: https://storybook.js.org/docs/api/argtypes
args: { args: {
colors: getCourseColors('emerald'), colors: getCourseColors('emerald'),
course: exampleCourse, course: ExampleCourse,
}, },
argTypes: { argTypes: {
colors: { colors: {
@@ -87,15 +93,15 @@ export const Default: Story = {
export const Variants: Story = { export const Variants: Story = {
render: props => ( render: props => (
<div className='grid grid-cols-2 max-w-2xl w-90vw gap-x-4 gap-y-2'> <div className='grid grid-cols-2 max-w-2xl w-90vw gap-x-4 gap-y-2'>
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.OPEN })} /> <PopupCourseBlock {...props} course={new Course({ ...ExampleCourse, status: Status.OPEN })} />
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CLOSED })} /> <PopupCourseBlock {...props} course={new Course({ ...ExampleCourse, status: Status.CLOSED })} />
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.WAITLISTED })} /> <PopupCourseBlock {...props} course={new Course({ ...ExampleCourse, status: Status.WAITLISTED })} />
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CANCELLED })} /> <PopupCourseBlock {...props} course={new Course({ ...ExampleCourse, status: Status.CANCELLED })} />
</div> </div>
), ),
}; };
export const test_colors = Object.keys(theme.colors) export const TestColors = 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)
@@ -104,8 +110,8 @@ export const test_colors = Object.keys(theme.colors)
export const AllColors: Story = { export const AllColors: Story = {
render: props => ( render: props => (
<div className='grid grid-flow-col grid-cols-2 grid-rows-9 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'>
{test_colors.map((color, i) => ( {TestColors.map((color, i) => (
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} /> <PopupCourseBlock key={color.primaryColor} course={ExampleCourse} colors={color} />
))} ))}
</div> </div>
), ),

View File

@@ -0,0 +1,19 @@
import type { 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: {},
};

View File

@@ -1,4 +1,4 @@
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses'; import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
const meta = { const meta = {
@@ -11,7 +11,7 @@ const meta = {
argTypes: { argTypes: {
scheduleName: { control: 'text' }, scheduleName: { control: 'text' },
totalHours: { control: 'number' }, totalHours: { control: 'number' },
totalCourses: { control: 'number' } totalCourses: { control: 'number' },
}, },
} satisfies Meta<typeof ScheduleTotalHoursAndCourses>; } satisfies Meta<typeof ScheduleTotalHoursAndCourses>;
export default meta; export default meta;
@@ -22,6 +22,6 @@ export const Default: Story = {
args: { args: {
scheduleName: 'SCHEDULE', scheduleName: 'SCHEDULE',
totalHours: 22, totalHours: 22,
totalCourses: 8 totalCourses: 8,
}, },
}; };

View File

@@ -1,5 +1,6 @@
/* eslint-disable jsdoc/require-jsdoc */
import ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem';
import React from 'react'; import React from 'react';
import ScheduleListItem from 'src/views/components/common/ScheduleListItem/ScheduleListItem';
export default { export default {
title: 'Components/Common/ScheduleListItem', title: 'Components/Common/ScheduleListItem',
@@ -14,21 +15,21 @@ export default {
}, },
}; };
export const Default = (args) => <ScheduleListItem {...args} />; export const Default = args => <ScheduleListItem {...args} />;
Default.args = { Default.args = {
name: 'My Schedule', name: 'My Schedule',
active: true, active: true,
}; };
export const Active = (args) => <ScheduleListItem {...args} />; export const Active = args => <ScheduleListItem {...args} />;
Active.args = { Active.args = {
name: 'My Schedule', name: 'My Schedule',
active: true, active: true,
}; };
export const Inactive = (args) => <ScheduleListItem {...args} />; export const Inactive = args => <ScheduleListItem {...args} />;
Inactive.args = { Inactive.args = {
name: 'My Schedule', name: 'My Schedule',

View File

@@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Settings from 'src/views/components/Settings'; import Settings from '@views/components/Settings';
const meta = { const meta = {
title: 'Components/Common/Settings', title: 'Components/Common/Settings',

View File

@@ -1,5 +1,5 @@
import Spinner from 'src/views/components/common/Spinner/Spinner';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Spinner from '@views/components/common/Spinner/Spinner';
const meta = { const meta = {
title: 'Components/Common/Spinner', title: 'Components/Common/Spinner',

View File

@@ -1,7 +1,6 @@
import { Button } from 'src/views/components/common/Button/Button';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Text from '@views/components/common/Text/Text';
import React from 'react'; import React from 'react';
import Text from '../../views/components/common/Text/Text';
// 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 = {

View File

@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Calendar } from '@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: {},
};

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Course, Status } from '@shared/types/Course'; import { Course, Status } from '@shared/types/Course';
import Instructor from '@shared/types/Instructor'; import Instructor from '@shared/types/Instructor';
import { CalendarBottomBar } from '@views/components/common/CalendarBottomBar/CalendarBottomBar'; import { getCourseColors } from '@shared/util/colors';
import { getCourseColors } from '../../shared/util/colors'; import type { Meta, StoryObj } from '@storybook/react';
import { CalendarBottomBar } from '@views/components/calendar/CalendarBottomBar/CalendarBottomBar';
import React from 'react';
const exampleGovCourse: Course = new Course({ const exampleGovCourse: Course = new Course({
courseName: 'Nope', courseName: 'Nope',
@@ -66,7 +66,7 @@ const examplePsyCourse: Course = new Course({
}); });
const meta = { const meta = {
title: 'Components/Common/CalendarBottomBar', title: 'Components/Calendar/CalendarBottomBar',
component: CalendarBottomBar, component: CalendarBottomBar,
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@@ -1,12 +1,12 @@
import { Meta, StoryObj } from '@storybook/react';
import { Course, Status } from '@shared/types/Course'; import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting'; import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule'; import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor'; import Instructor from '@shared/types/Instructor';
import CalendarCourse from '@views/components/common/CalendarCourseBlock/CalendarCourseMeeting'; import type { Meta, StoryObj } from '@storybook/react';
import CalendarCourse from '@views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting';
const meta = { const meta = {
title: 'Components/Common/CalendarCourseMeeting', title: 'Components/Calendar/CalendarCourseMeeting',
component: CalendarCourse, component: CalendarCourse,
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@@ -0,0 +1,115 @@
import { Course, Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import type { Meta, StoryObj } from '@storybook/react';
import CalendarCourseCell from '@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>
),
};

View File

@@ -0,0 +1,121 @@
import { Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import type { Meta, StoryObj } from '@storybook/react';
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
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 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),
},
},
{
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),
},
},
{
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),
},
},
{
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),
},
},
{
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),
},
},
{
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),
},
},
{
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),
},
},
];
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
saturdayClass: true,
courseCells: testData,
},
};

View File

@@ -0,0 +1,18 @@
// Calendar.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import CalendarCell from '@views/components/calendar/CalendarGridCell/CalendarGridCell';
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 = {};

View File

@@ -1,9 +1,8 @@
import React from 'react'; import type { Meta, StoryObj } from '@storybook/react';
import { Meta, StoryObj } from '@storybook/react'; import CalendarHeader from '@views/components/calendar/CalendarHeader/CalenderHeader';
import CalendarHeader from '@views/components/common/CalendarHeader/CalenderHeader';
const meta = { const meta = {
title: 'Components/Common/CalendarHeader', title: 'Components/Calendar/CalendarHeader',
component: CalendarHeader, component: CalendarHeader,
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@@ -1,14 +1,14 @@
import { Course, Status } from '@shared/types/Course'; import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor';
import { UserSchedule } from '@shared/types/UserSchedule'; import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
import React from '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/common/CalendarSchedules/CalendarSchedules';
const meta = { const meta = {
title: 'Components/Common/CalendarSchedules', title: 'Components/Calendar/CalendarSchedules',
component: CalendarSchedules, component: CalendarSchedules,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
@@ -64,6 +64,7 @@ const schedules = [
}), }),
], ],
name: 'Main Schedule', name: 'Main Schedule',
hours: 0, // Add the missing 'hours' property
}), }),
new UserSchedule({ new UserSchedule({
courses: [ courses: [
@@ -131,6 +132,7 @@ const schedules = [
}), }),
], ],
name: 'Backup #3', name: 'Backup #3',
hours: 0, // Add the missing 'hours' property
}), }),
]; ];

View File

@@ -0,0 +1,68 @@
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor';
import type { Meta, StoryObj } from '@storybook/react';
import CourseCatalogInjectedPopup from '@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 meta: Meta<typeof CourseCatalogInjectedPopup> = {
title: 'Components/Injected/CourseCatalogInjectedPopup',
component: CourseCatalogInjectedPopup,
argTypes: {
onClose: { action: 'onClose' },
},
};
export default meta;
type Story = StoryObj<typeof CourseCatalogInjectedPopup>;
export const Default: Story = {
args: {
course: exampleCourse,
},
};

View File

@@ -1,9 +1,9 @@
import { Course, Status } from 'src/shared/types/Course'; import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting'; import { CourseMeeting } from '@shared/types/CourseMeeting';
import { UserSchedule } from 'src/shared/types/UserSchedule'; import Instructor from '@shared/types/Instructor';
import CoursePopup from 'src/views/components/injected/CoursePopup/CoursePopup'; import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Instructor from 'src/shared/types/Instructor'; import CoursePopup from '@views/components/injected/CoursePopupOld/CoursePopup';
const exampleCourse: Course = new Course({ const exampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB', courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
@@ -57,6 +57,7 @@ const exampleCourse: Course = new Course({
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 = {
@@ -96,6 +97,7 @@ export const Open: Story = {
activeSchedule: new UserSchedule({ activeSchedule: new UserSchedule({
courses: [], courses: [],
name: 'Example Schedule', name: 'Example Schedule',
hours: 0,
}), }),
}, },
}; };

View File

@@ -2,13 +2,7 @@
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"lib": [ "lib": ["DOM", "es2021"],
"DOM", "types": ["chrome", "node"]
"es2021" }
],
"types": [
"chrome",
"node",
],
},
} }

View File

@@ -1,14 +1,16 @@
import { Course, ScrapedRow } from '@shared/types/Course'; import type { Course, ScrapedRow } from '@shared/types/Course';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useKeyPress } from '../hooks/useKeyPress'; import { useKeyPress } from '../hooks/useKeyPress';
import useSchedules from '../hooks/useSchedules'; import useSchedules from '../hooks/useSchedules';
import { CourseCatalogScraper } from '../lib/CourseCatalogScraper'; import { CourseCatalogScraper } from '../lib/CourseCatalogScraper';
import getCourseTableRows from '../lib/getCourseTableRows'; import getCourseTableRows from '../lib/getCourseTableRows';
import { SiteSupport } from '../lib/getSiteSupport'; import type { 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 CourseCatalogInjectedPopup from './injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
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';
@@ -79,7 +81,7 @@ export default function CourseCatalogMain({ support }: Props) {
); );
})} })}
{selectedCourse && ( {selectedCourse && (
<CoursePopup <CourseCatalogInjectedPopup
course={selectedCourse} course={selectedCourse}
activeSchedule={activeSchedule} activeSchedule={activeSchedule}
onClose={handleClearSelectedCourse} onClose={handleClearSelectedCourse}

View File

@@ -1,24 +1,161 @@
import { background } from '@shared/messages'; import logoImage from '@assets/logo.png'; // Adjust the path as necessary
import { Status } from '@shared/types/Course';
import { StatusIcon } from '@shared/util/icons';
import Divider from '@views/components/common/Divider/Divider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import List from '@views/components/common/List/List'; // Ensure this path is correctly pointing to your List component
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import Text from '@views/components/common/Text/Text';
import { handleOpenCalendar } from '@views/components/injected/CourseCatalogInjectedPopup/HeadingAndActions';
import useSchedules from '@views/hooks/useSchedules';
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
import React from 'react'; import React from 'react';
import useSchedules from '../hooks/useSchedules'; import { FaCalendarAlt, FaCog, FaRedo } from 'react-icons/fa'; // Added FaRedo for the refresh icon
import { Button } from './common/Button/Button'; import { TestColors } from 'src/stories/components/PopupCourseBlock.stories';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
/**
* Renders the main popup component.
* This component displays the main schedule, courses, and options buttons.
*/
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={TestColors[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' }}>
UT Registration
</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',
}} }}
> >
Clear Courses <StatusIcon status={Status.WAITLISTED} className='h-5 w-5 text-white' />
</Button> </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>
); );
} }

View File

@@ -0,0 +1,41 @@
import React from 'react';
import CalendarHeader from 'src/views/components/calendar/CalendarHeader/CalenderHeader';
import { CalendarBottomBar } from '../CalendarBottomBar/CalendarBottomBar';
import CalendarGrid from '../CalendarGrid/CalendarGrid';
import { CalendarSchedules } from '../CalendarSchedules/CalendarSchedules';
import ImportantLinks from '../ImportantLinks';
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>
</>
);
}

View File

@@ -1,26 +1,29 @@
import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Text from '../Text/Text'; import React from 'react';
import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
import { Button } from '../Button/Button';
import ImageIcon from '~icons/material-symbols/image';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
import ImageIcon from '~icons/material-symbols/image';
import { Button } from '../../common/Button/Button';
import Text from '../../common/Text/Text';
import type { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
import CalendarCourseBlock from '../CalendarCourseCell/CalendarCourseCell';
type CalendarBottomBarProps = { type CalendarBottomBarProps = {
courses: CalendarCourseCellProps[]; courses?: CalendarCourseCellProps[];
}; };
/** /**
* *
*/ */
export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => { export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => {
if (courses.length === -1) console.log('foo'); // dumb line to make eslint happy if (courses?.length === -1) console.log('foo'); // dumb line to make eslint happy
return ( return (
<div className='w-full flex py-1.25'> <div className='w-full flex py-1.25'>
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'> <div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
<Text variant='h4'>Async. and Other:</Text> <Text variant='h4'>Async. and Other:</Text>
<div className='h-14 inline-flex gap-2.5'> <div className='h-14 inline-flex gap-2.5'>
{courses.map(course => ( {courses?.map(course => (
<CalendarCourseBlock <CalendarCourseBlock
courseDeptAndInstr={course.courseDeptAndInstr} courseDeptAndInstr={course.courseDeptAndInstr}
status={course.status} status={course.status}

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Course } from 'src/shared/types/Course'; import type { Course } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting'; import type { CourseMeeting } from 'src/shared/types/CourseMeeting';
import styles from './CalendarCourseMeeting.module.scss'; import styles from './CalendarCourseMeeting.module.scss';
/** /**
@@ -26,6 +27,8 @@ export interface CalendarCourseMeetingProps {
const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({ const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
course, course,
meetingIdx, meetingIdx,
color,
rightIcon,
}: CalendarCourseMeetingProps) => { }: CalendarCourseMeetingProps) => {
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null; let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
return ( return (

View File

@@ -1,12 +1,17 @@
import { Status } from '@shared/types/Course'; import { Status } from '@shared/types/Course';
import Text from '@views/components/common/Text/Text';
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import { CourseColors, pickFontColor } from 'src/shared/util/colors'; import type { CourseColors } from 'src/shared/util/colors';
import { pickFontColor } from 'src/shared/util/colors';
import ClosedIcon from '~icons/material-symbols/lock'; import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse'; import WaitlistIcon from '~icons/material-symbols/timelapse';
import CancelledIcon from '~icons/material-symbols/warning'; import CancelledIcon from '~icons/material-symbols/warning';
import Text from '../Text/Text';
/**
* Props for the CalendarCourseCell component.
*/
export interface CalendarCourseCellProps { export interface CalendarCourseCellProps {
courseDeptAndInstr: string; courseDeptAndInstr: string;
timeAndLocation?: string; timeAndLocation?: string;
@@ -15,6 +20,18 @@ export interface CalendarCourseCellProps {
className?: string; className?: string;
} }
/**
* Renders a cell for a calendar course.
*
* @component
* @param {CalendarCourseCellProps} props - The component props.
* @param {string} props.courseDeptAndInstr - The course department and instructor.
* @param {string} props.timeAndLocation - The time and location of the course.
* @param {Status} props.status - The status of the course.
* @param {Colors} props.colors - The colors for styling the cell.
* @param {string} props.className - Additional CSS class name for the cell.
* @returns {JSX.Element} The rendered component.
*/
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
courseDeptAndInstr, courseDeptAndInstr,
timeAndLocation, timeAndLocation,
@@ -36,12 +53,12 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
return ( return (
<div <div
className={clsx('w-full flex justify-center rounded p-2', fontColor, className)} className={clsx('h-full w-full flex justify-center rounded p-2 overflow-x-hidden', fontColor, className)}
style={{ style={{
backgroundColor: colors.primaryColor, backgroundColor: colors.primaryColor,
}} }}
> >
<div className='flex flex-1 flex-col gap-1 overflow-x-hidden'> <div className='flex flex-1 flex-col gap-1'>
<Text <Text
variant='h1-course' variant='h1-course'
className={clsx('-my-0.8 leading-tight', { className={clsx('-my-0.8 leading-tight', {

View File

@@ -13,8 +13,10 @@
.calendarGrid { .calendarGrid {
display: grid; display: grid;
grid-template-columns: repeat(6, 1fr); grid-template-columns: auto repeat(5, 6fr);
grid-template-rows: repeat(13, 1fr); grid-template-rows: repeat(26, 1fr);
width: 100%;
height: 100%;
} }
.calendarRow { .calendarRow {
@@ -25,7 +27,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
position: relative; // Ensuring that child elements can be positioned in relation to this. position: relative;
width: 100%;
height: 100%;
} }
.day { .day {
@@ -53,7 +57,7 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
flex: 1 0 0; flex: 1 0 0;
border-radius: var(--border-radius-none, 0px); border-radius: 0px;
} }
.timeBlock { .timeBlock {
@@ -125,3 +129,12 @@
width: 1px; width: 1px;
background-color: grey; 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 */
}

View File

@@ -0,0 +1,223 @@
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import React, { useEffect, useRef, useState } from 'react';
// import html2canvas from 'html2canvas';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
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);
}, [hoursOfDay]);
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}
/>
</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> */

Some files were not shown because too many files have changed in this diff Show More