Compare commits

..

26 Commits

Author SHA1 Message Date
Casey Charleston
2c4dd36f27 feat: popup misaligned but clickable on calendar grid 2024-02-23 09:52:53 -06:00
Casey Charleston
5daccc8349 fix: injected course popup storybook 2024-02-21 23:51:12 -06:00
Casey Charleston
e4ce051086 Merging hackathon into hackathon 2024-02-21 23:28:32 -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
Casey Charleston
51b6799d73 dropdown and calendar schedules storybook 2024-02-17 18:35:17 -06:00
Casey Charleston
141f5cc70e merging 2024-02-17 18:00:06 -06:00
Casey Charleston
6883a0bc8d packages 2024-02-17 17:59:05 -06:00
vivek12311
ff4ee494b6 Using React-icons. Probably need to take out later 2024-02-17 16:03:28 -06:00
78 changed files with 3727 additions and 1028 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",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix",
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"preview": "vite preview", "preview": "vite preview",
"devtools": "react-devtools", "devtools": "react-devtools",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build",
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@hello-pangea/dnd": "^16.5.0", "@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",
@@ -29,29 +38,31 @@
"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", "react-window": "^1.8.10",
"sass": "^1.70.0", "sass": "^1.71.1",
"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 +74,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 +88,27 @@
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.0.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": {

2045
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

@@ -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,6 @@
import React from 'react'; import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
/** /**
* Calendar page * 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

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

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

@@ -6,7 +6,7 @@ export interface CourseColors {
} }
// calculates luminance of a hex string // calculates luminance of a hex string
function getLuminance(hex: string): number { export function getLuminance(hex: string): number {
let r = parseInt(hex.substring(1, 3), 16); let r = parseInt(hex.substring(1, 3), 16);
let g = parseInt(hex.substring(3, 5), 16); let g = parseInt(hex.substring(3, 5), 16);
let b = parseInt(hex.substring(5, 7), 16); let b = parseInt(hex.substring(5, 7), 16);

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,39 @@
import { describe, expect, it } from 'vitest';
import { capitalize } from '../string';
// TODO: Fix `string.ts` and `string.test.ts` to make the tests pass
// `capitalize` is adding an extra space at the end of the word.
describe('capitalize', () => {
it('should capitalize the first letter of each word', () => {
// Debug
const word = 'hello world';
const capitalized = capitalize(word);
console.log(capitalize(word));
console.log(capitalized.length);
console.log(capitalized.split(''));
// Test case 1: Single word
expect(capitalize('hello')).toBe('Hello');
// Test case 2: Multiple words
expect(capitalize('hello world')).toBe('Hello World');
// Test case 3: Words with hyphens
expect(capitalize('hello-world')).toBe('Hello-World');
// Test case 4: Words with hyphens and spaces
expect(capitalize('hello-world test')).toBe('Hello-World Test');
});
it('should not change the capitalization of the remaining letters', () => {
// Test case 1: All lowercase
expect(capitalize('hello')).toBe('Hello');
// Test case 2: All uppercase
expect(capitalize('WORLD')).toBe('WORLD');
// Test case 3: Mixed case
expect(capitalize('HeLLo WoRLd')).toBe('Hello World');
});
});

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

@@ -11,11 +11,26 @@ export const colors = {
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',
}, },
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; } as const;
type NestedKeys<T> = { type NestedKeys<T> = {

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

@@ -0,0 +1,157 @@
import { Course, Status } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import type { Serialized } from 'chrome-extension-toolkit';
import React from 'react';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import Dropdown from 'src/views/components/common/Dropdown/Dropdown';
import ScheduleListItem from 'src/views/components/common/ScheduleListItem/ScheduleListItem';
const meta: Meta<typeof Dropdown> = {
title: 'Components/Common/Dropdown',
component: Dropdown,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
dummySchedules: { control: 'object' },
dummyActiveIndex: { control: 'number' },
scheduleComponents: { control: 'object' },
},
render: (args: any) => (
<div className='w-80'>
<Dropdown {...args} />
</div>
),
} satisfies Meta<typeof Dropdown>;
export default meta;
type Story = StoryObj<typeof meta>;
const schedules = [
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
],
name: 'Main Schedule',
hours: 0,
} as Serialized<UserSchedule>),
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
],
name: 'Backup #3',
hours: 0,
} as Serialized<UserSchedule>),
];
export const Hidden: Story = {
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1579-5083&mode=dev',
},
},
args: {
dummySchedules: schedules,
dummyActiveIndex: 0,
scheduleComponents: schedules.map((schedule, index) => (
<ScheduleListItem active={index === 0} name={schedule.name} />
)),
},
};

View File

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

View File

@@ -9,7 +9,7 @@ import { test_colors } from './PopupCourseBlock.stories';
const numberOfCourses = 5; const numberOfCourses = 5;
const generateCourses = count => { export const generateCourses = count => {
const courses = []; const courses = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
@@ -64,7 +64,7 @@ const generateCourses = count => {
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, test_colors);
const meta = { const meta = {
title: 'Components/Common/List', title: 'Components/Common/List',

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { Meta, StoryObj } from '@storybook/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 { CalendarBottomBar } from 'src/views/components/calendar/CalendarBottomBar/CalendarBottomBar';
import { getCourseColors } from '../../shared/util/colors'; import { getCourseColors } from '../../../shared/util/colors';
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

@@ -3,10 +3,10 @@ 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 CalendarCourse from 'src/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,114 @@
import { Course, Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import { Meta, StoryObj } from '@storybook/react';
import CalendarCourseCell from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import React from 'react';
import { exampleCourse } from '../PopupCourseBlock.stories';
const meta = {
title: 'Components/Calendar/CalendarCourseCell',
component: CalendarCourseCell,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
courseDeptAndInstr: { control: { type: 'text' } },
className: { control: { type: 'text' } },
status: { control: { type: 'select', options: Object.values(Status) } },
timeAndLocation: { control: { type: 'text' } },
colors: { control: { type: 'object' } },
},
render: (args: any) => (
<div className='w-45'>
<CalendarCourseCell {...args} />
</div>
),
args: {
courseDeptAndInstr: exampleCourse.department,
className: exampleCourse.number,
status: exampleCourse.status,
timeAndLocation: exampleCourse.schedule.meetings[0].getTimeString({ separator: '-' }),
colors: getCourseColors('emerald', 500),
},
} satisfies Meta<typeof CalendarCourseCell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Variants: Story = {
render: props => (
<div className='grid grid-cols-2 h-40 max-w-60 w-90vw gap-x-4 gap-y-2'>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.OPEN })}
// Course = new Course({
// courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
// creditHours: 3,
// department: 'C S',
// description: [
// 'Restricted to computer science majors.',
// 'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
// 'Computer Science 439 and 439H may not both be counted.',
// 'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
// 'May be counted toward the Independent Inquiry flag requirement.',
// ],
// flags: ['Independent Inquiry'],
// fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
// instructionMode: 'In Person',
// instructors: [
// new Instructor({
// firstName: 'Allison',
// lastName: 'Norman',
// fullName: 'Allison Norman',
// }),
// ],
// isReserved: false,
// number: '439',
// schedule: {
// meetings: [
// new CourseMeeting({
// days: ['Tuesday', 'Thursday'],
// startTime: 930,
// endTime: 1050,
// }),
// new CourseMeeting({
// days: ['Friday'],
// startTime: 600,
// endTime: 720,
// }),
// ],
// },
// semester: {
// code: '12345',
// season: 'Spring',
// year: 2024,
// },
// status: Status.WAITLISTED,
// uniqueId: 67890,
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
// });
colors={getCourseColors('green', 500)}
/>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.CLOSED })}
colors={getCourseColors('teal', 400)}
/>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.WAITLISTED })}
colors={getCourseColors('indigo', 400)}
/>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.CANCELLED })}
colors={getCourseColors('red', 500)}
/>
</div>
),
};

View File

@@ -0,0 +1,212 @@
import { Course, Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import type { Meta, StoryObj } from '@storybook/react';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import { Serialized } from 'chrome-extension-toolkit';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import CalendarGrid from 'src/views/components/calendar/CalendarGrid/CalendarGrid';
const meta = {
title: 'Components/Calendar/CalendarGrid',
component: CalendarGrid,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
saturdayClass: { control: 'boolean' },
},
} satisfies Meta<typeof CalendarGrid>;
export default meta;
const exampleCourse: Course = new Course({
uniqueId: 50805,
number: '314',
fullName: 'C S 314 DATA STRUCTURES',
courseName: 'DATA STRUCTURES',
department: 'C S',
creditHours: 3,
status: Status.OPEN,
instructors: [
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
],
isReserved: true,
description: [
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
'Computer Science 314 and 314H may not both be counted.',
'BVO 311C and 312H may not both be counted.',
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
'May be counted toward the Quantitative Reasoning flag requirement.',
],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.T, DAY_MAP.TH],
startTime: 480,
endTime: 570,
location: { building: 'UTC', room: '123' },
}),
new CourseMeeting({
days: [DAY_MAP.TH],
startTime: 570,
endTime: 630,
location: { building: 'JES', room: '123' },
}),
],
}),
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Writing', 'Independent Inquiry'],
instructionMode: 'In Person',
semester: {
code: '12345',
year: 2024,
season: 'Spring',
},
});
const exampleSchedule = new UserSchedule({
name: 'Example Schedule',
courses: [exampleCourse],
hours: 3,
} as Serialized<UserSchedule>);
const testData: CalendarGridCourse[] = [
{
calendarGridPoint: {
dayIndex: 4,
startIndex: 10,
endIndex: 11,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 2,
startIndex: 5,
endIndex: 6,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 2',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 4,
startIndex: 10,
endIndex: 11,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 2',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 3',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 4',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
];
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
saturdayClass: true,
courseCells: testData,
},
};

View File

@@ -0,0 +1,19 @@
// Calendar.stories.tsx
import React from 'react';
import CalendarCell from 'src/views/components/calendar/CalendarGridCell/CalendarGridCell';
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
title: 'Components/Calendar/CalendarGridCell',
component: CalendarCell,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
tags: ['autodocs'],
},
} satisfies Meta<typeof CalendarCell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

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

@@ -5,10 +5,10 @@ import React from 'react';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting'; import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule'; import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor'; import Instructor from 'src/shared/types/Instructor';
import { CalendarSchedules } from 'src/views/components/common/CalendarSchedules/CalendarSchedules'; import { CalendarSchedules } from 'src/views/components/calendar/CalendarSchedules/CalendarSchedules';
const meta = { const meta = {
title: 'Components/Common/CalendarSchedules', title: 'Components/Calendar/CalendarSchedules',
component: CalendarSchedules, component: CalendarSchedules,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
@@ -17,6 +17,7 @@ const meta = {
argTypes: { argTypes: {
dummySchedules: { control: 'object' }, dummySchedules: { control: 'object' },
dummyActiveIndex: { control: 'number' }, dummyActiveIndex: { control: 'number' },
}, },
render: (args: any) => ( render: (args: any) => (
<div> <div>
@@ -64,6 +65,7 @@ const schedules = [
}), }),
], ],
name: 'Main Schedule', name: 'Main Schedule',
hours: 0, // Add the missing 'hours' property
}), }),
new UserSchedule({ new UserSchedule({
courses: [ courses: [
@@ -131,6 +133,7 @@ const schedules = [
}), }),
], ],
name: 'Backup #3', name: 'Backup #3',
hours: 0, // Add the missing 'hours' property
}), }),
]; ];
@@ -138,5 +141,6 @@ export const Default: Story = {
args: { args: {
dummySchedules: schedules, dummySchedules: schedules,
dummyActiveIndex: 0, dummyActiveIndex: 0,
}, },
}; };

View File

@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { Serialized } from 'chrome-extension-toolkit';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
const exampleCourse: Course = new Course({
uniqueId: 50805,
number: '314',
fullName: 'C S 314 DATA STRUCTURES',
courseName: 'DATA STRUCTURES',
department: 'C S',
creditHours: 3,
status: Status.OPEN,
instructors: [
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
],
isReserved: true,
description: [
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
'Computer Science 314 and 314H may not both be counted.',
'BVO 311C and 312H may not both be counted.',
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
'May be counted toward the Quantitative Reasoning flag requirement.',
],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.T, DAY_MAP.TH],
startTime: 480,
endTime: 570,
location: { building: 'UTC', room: '123' },
}),
new CourseMeeting({
days: [DAY_MAP.TH],
startTime: 570,
endTime: 630,
location: { building: 'JES', room: '123' },
}),
],
}),
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Writing', 'Independent Inquiry'],
instructionMode: 'In Person',
semester: {
code: '12345',
year: 2024,
season: 'Spring',
},
});
const exampleSchedule = new UserSchedule({
name: 'Example Schedule',
courses: [exampleCourse],
hours: 3,
} as Serialized<UserSchedule>);
const meta: Meta<typeof CourseCatalogInjectedPopup> = {
title: 'Components/Injected/CourseCatalogInjectedPopup',
component: CourseCatalogInjectedPopup,
argTypes: {
onClose: { action: 'onClose' },
activeSchedule: { control: 'object' },
course: { control: 'object' },
},
};
export default meta;
type Story = StoryObj<typeof CourseCatalogInjectedPopup>;
export const Default: Story = {
args: {
activeSchedule: exampleSchedule,
course: exampleCourse,
},
};

View File

@@ -1,7 +1,7 @@
import { Course, Status } from 'src/shared/types/Course'; import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting'; import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import { UserSchedule } from 'src/shared/types/UserSchedule'; import { UserSchedule } from 'src/shared/types/UserSchedule';
import CoursePopup from 'src/views/components/injected/CoursePopup/CoursePopup'; import CoursePopup from 'src/views/components/injected/CoursePopupOld/CoursePopup';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import Instructor from 'src/shared/types/Instructor'; import Instructor from 'src/shared/types/Instructor';
@@ -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

@@ -8,11 +8,12 @@ import { SiteSupport } from '../lib/getSiteSupport';
import { populateSearchInputs } from '../lib/populateSearchInputs'; import { populateSearchInputs } from '../lib/populateSearchInputs';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
import AutoLoad from './injected/AutoLoad/AutoLoad'; import AutoLoad from './injected/AutoLoad/AutoLoad';
import CoursePopup from './injected/CoursePopup/CoursePopup'; import CoursePopup from './injected/CoursePopupOld/CoursePopup';
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner'; import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
import TableHead from './injected/TableHead'; import TableHead from './injected/TableHead';
import TableRow from './injected/TableRow/TableRow'; import TableRow from './injected/TableRow/TableRow';
import TableSubheading from './injected/TableSubheading/TableSubheading'; import TableSubheading from './injected/TableSubheading/TableSubheading';
import CourseCatalogInjectedPopup from './injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
interface Props { interface Props {
support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST; support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST;
@@ -79,7 +80,7 @@ export default function CourseCatalogMain({ support }: Props) {
); );
})} })}
{selectedCourse && ( {selectedCourse && (
<CoursePopup <CourseCatalogInjectedPopup
course={selectedCourse} course={selectedCourse}
activeSchedule={activeSchedule} activeSchedule={activeSchedule}
onClose={handleClearSelectedCourse} onClose={handleClearSelectedCourse}

View File

@@ -1,24 +1,157 @@
import { background } from '@shared/messages'; import { StatusIcon } from '@shared/util/icons';
import React from 'react'; import React from 'react';
// import { FaCalendarAlt, FaCog, FaRedo } from 'react-icons/fa'; // Added FaRedo for the refresh icon
import { Status } from 'src/shared/types/Course';
import { test_colors } from 'src/stories/components/PopupCourseBlock.stories';
import logoImage from '../../assets/logo.png'; // Adjust the path as necessary
import useSchedules from '../hooks/useSchedules'; import useSchedules from '../hooks/useSchedules';
import { Button } from './common/Button/Button'; import { openTabFromContentScript } from '../lib/openNewTabFromContentScript';
import Divider from './common/Divider/Divider';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
import List from './common/List/List'; // Ensure this path is correctly pointing to your List component
import PopupCourseBlock from './common/PopupCourseBlock/PopupCourseBlock';
import Text from './common/Text/Text';
import { handleOpenCalendar } from './injected/CourseCatalogInjectedPopup/HeadingAndActions';
export default function PopupMain() { export default function PopupMain() {
const [activeSchedule, schedules] = useSchedules(); const [activeSchedule] = useSchedules();
// TODO: Add a button to to switch the active schedule const draggableElements = activeSchedule?.courses.map((course, i) => (
<PopupCourseBlock key={course.uniqueId} course={course} colors={test_colors[i]} />
));
const handleOpenOptions = async () => {
// Not sure if it's bad practice to export this
const url = chrome.runtime.getURL('/src/pages/options/index.html');
await openTabFromContentScript(url);
};
return ( return (
<ExtensionRoot> <ExtensionRoot>
<Button <div className='mx-auto max-w-sm rounded-lg bg-white p-4 shadow-md'>
onClick={() => { <div className='mb-2 flex items-center justify-between bg-white'>
if (!activeSchedule) return; <div className='flex items-center'>
background.clearCourses({ scheduleName: activeSchedule?.name }); <img src={logoImage} alt='Logo' style={{ width: '40px', height: '40px', marginRight: '8px' }} />
<div>
<Text as='div' variant='h1-course' style={{ color: '#bf5700', fontSize: '1.3rem' }}>
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,40 @@
import React from 'react';
import CalendarHeader from 'src/views/components/calendar/CalendarHeader/CalenderHeader';
import { CalendarBottomBar } from '../CalendarBottomBar/CalendarBottomBar';
import { CalendarSchedules } from '../CalendarSchedules/CalendarSchedules';
import ImportantLinks from '../ImportantLinks';
import CalendarGrid from '../CalendarGrid/CalendarGrid';
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];
interface Props {
label: string;
}
/**
* A reusable chip component that follows the design system of the extension.
* @returns
*/
export function Calendar(): JSX.Element {
return (
<>
<CalendarHeader />
<div className='h-screen w-full flex flex-col md:flex-row'>
<div className='min-h-[30%] flex flex-col items-start gap-2.5 p-5 pl-7'>
<div className='min-h-[30%]'>
<CalendarSchedules />
</div>
<ImportantLinks />
</div>
<div className='flex flex-grow flex-col gap-4 overflow-hidden'>
<div className='flex-grow overflow-auto'>
<CalendarGrid />
</div>
<div>
<CalendarBottomBar />
</div>
</div>
</div>
</>
);
}

View File

@@ -1,26 +1,26 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Text from '../Text/Text'; import Text from '../../common/Text/Text';
import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell'; import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
import { Button } from '../Button/Button'; import { Button } from '../../common/Button/Button';
import ImageIcon from '~icons/material-symbols/image'; import ImageIcon from '~icons/material-symbols/image';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
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,11 +1,14 @@
import { Status } from '@shared/types/Course'; import { Status } from '@shared/types/Course';
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';
import Text from '../../common/Text/Text';
export interface CalendarCourseCellProps { export interface CalendarCourseCellProps {
courseDeptAndInstr: string; courseDeptAndInstr: string;
@@ -13,6 +16,7 @@ export interface CalendarCourseCellProps {
status: Status; status: Status;
colors: CourseColors; colors: CourseColors;
className?: string; className?: string;
popup?: any;
} }
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
@@ -21,7 +25,9 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
status, status,
colors, colors,
className, className,
popup,
}: CalendarCourseCellProps) => { }: CalendarCourseCellProps) => {
const [showPopup, setShowPopup] = React.useState(false);
let rightIcon: React.ReactNode | null = null; let rightIcon: React.ReactNode | null = null;
if (status === Status.WAITLISTED) { if (status === Status.WAITLISTED) {
rightIcon = <WaitlistIcon className='h-5 w-5' />; rightIcon = <WaitlistIcon className='h-5 w-5' />;
@@ -31,17 +37,28 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
rightIcon = <CancelledIcon className='h-5 w-5' />; rightIcon = <CancelledIcon className='h-5 w-5' />;
} }
// popup.onClose = () => setShowPopup(false);
const handleClick = () => {
setShowPopup(true);
};
// whiteText based on secondaryColor // whiteText based on secondaryColor
const fontColor = pickFontColor(colors.primaryColor); const fontColor = pickFontColor(colors.primaryColor);
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 cursor-pointer',
fontColor,
className
)}
style={{ style={{
backgroundColor: colors.primaryColor, backgroundColor: colors.primaryColor,
}} }}
onClick={handleClick}
> >
<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', {
@@ -66,6 +83,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
{rightIcon} {rightIcon}
</div> </div>
)} )}
<div>{showPopup ? popup : null}</div>
</div> </div>
); );
}; };

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,226 @@
import React, { useEffect, useRef, useState } from 'react';
// import html2canvas from 'html2canvas';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import type { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
import CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
/* import calIcon from 'src/assets/icons/cal.svg';
import pngIcon from 'src/assets/icons/png.svg';
*/
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
import styles from './CalendarGrid.module.scss';
/* const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
const grid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
row.push(
<div key={hour} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
</div>
);
row.push(Array.from({ length: 5 }, (_, j) => <CalendarCell key={j} />));
grid.push(row);
} */
interface Props {
courseCells?: CalendarGridCourse[];
saturdayClass?: boolean;
}
/**
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
const [grid, setGrid] = useState([]);
const calendarRef = useRef(null); // Create a ref for the calendar grid
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
/* const saveAsPNG = () => {
htmlToImage
.toPng(calendarRef.current, {
backgroundColor: 'white',
style: {
background: 'white',
marginTop: '20px',
marginBottom: '20px',
marginRight: '20px',
marginLeft: '20px',
},
})
.then(dataUrl => {
let img = new Image();
img.src = dataUrl;
fetch(dataUrl)
.then(response => response.blob())
.then(blob => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = 'my-schedule.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(error => console.error('Error downloading file:', error));
})
.catch(error => {
console.error('oops, something went wrong!', error);
});
}; */
useEffect(() => {
const newGrid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
let styleProp = {
gridColumn: '1',
gridRow: `${2 * i + 2}`,
};
row.push(
<div key={hour} className={styles.timeBlock} style={styleProp}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
</div>
);
for (let k = 0; k < 5; k++) {
// let shouldRender = false;
styleProp = {
gridColumn: `${k + 2}`,
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
};
/* let shouldRenderChild = courseCells[iterator]?.calendarGridPoint &&
k === courseCells[iterator].calendarGridPoint.dayIndex && i === courseCells[iterator].calendarGridPoint.startIndex;
let childElement = <div className={styles.dot}/>; */
/* let completeGridCell = shouldRenderChild ? <CalendarCell key={k} children={childElement}/>
: <CalendarCell key={k} />; */
row.push(<CalendarCell key={k} styleProp={styleProp} />);
}
newGrid.push(row);
}
setGrid(newGrid);
}, []);
return (
<div className={styles.calendarGrid}>
{/* Displaying day labels */}
<div className={styles.timeBlock} />
{daysOfWeek.map(day => (
<div key={day} className={styles.day}>
{day}
</div>
))}
{grid.map((row, rowIndex) => row)}
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} /> : null}
{/* courseCells.map((block: CalendarGridCourse) => (
<div
key={`${block}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
}}
>
<CalendarCourseCell
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
timeAndLocation={block.componentProps.timeAndLocation}
status={block.componentProps.status}
colors={block.componentProps.colors}
/>
</div>
)) */}
</div>
);
}
export default CalendarGrid;
interface AccountForCourseConflictsProps {
courseCells: CalendarGridCourse[];
}
function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsProps): JSX.Element[] {
// Groups by dayIndex to identify overlaps
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
const { dayIndex } = cell.calendarGridPoint;
if (!acc[dayIndex]) {
acc[dayIndex] = [];
}
acc[dayIndex].push(cell);
return acc;
}, {});
// Check for overlaps within each day and adjust gridColumnIndex and totalColumns
Object.values(days).forEach((dayCells: CalendarGridCourse[]) => {
// Sort by start time to ensure proper columnIndex assignment
dayCells.sort((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
dayCells.forEach((cell, _, arr) => {
let columnIndex = 1;
cell.totalColumns = 1;
// Check for overlaps and adjust columnIndex as needed
for (let otherCell of arr) {
if (otherCell !== cell) {
const isOverlapping =
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
if (isOverlapping) {
console.log('Found overlapping element');
// Adjust columnIndex to not overlap with the otherCell
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
columnIndex = otherCell.gridColumnStart + 1;
console.log(columnIndex);
}
cell.totalColumns += 1;
}
}
}
cell.gridColumnStart = columnIndex;
cell.gridColumnEnd = columnIndex + 1;
});
});
return courseCells.map(block => (
<div
key={`${block}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
width: `calc(100% / ${block.totalColumns})`,
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
padding: '0px 10px 4px 0px',
}}
>
<CalendarCourseCell
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
timeAndLocation={block.componentProps.timeAndLocation}
status={block.componentProps.status}
colors={block.componentProps.colors}
popup={<CourseCatalogInjectedPopup course={block.popupProps.course} activeSchedule={block.popupProps.activeSchedule} onClose={block.popupProps.onClose} />}
/>
</div>
));
}
/* <div className={styles.buttonContainer}>
<div className={styles.divider} />
<button className={styles.calendarButton}>
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
Save as .CAL
</button>
<div className={styles.divider} />
<button onClick={saveAsPNG} className={styles.calendarButton}>
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
Save as .PNG
</button>
</div> */

View File

@@ -1,7 +1,7 @@
.calendarCell { .calendarCell {
display: flex; display: flex;
width: 213.8px; width: 100%;
height: 44.769px; height: 100%;
min-width: 45px; min-width: 45px;
min-height: 40px; min-height: 40px;
flex-direction: column; flex-direction: column;
@@ -11,8 +11,8 @@
} }
.hourLine { .hourLine {
width: 213.8px; width: 100%;
height: 1px; height: 1px;
border-radius: var(--border-radius-none, 0px); border-radius: 0px;
background: rgba(218, 220, 224, 0.25); background: rgba(218, 220, 224, 0.25);
} }

View File

@@ -0,0 +1,21 @@
import React from 'react';
import styles from './CalendarGridCell.module.scss';
interface Props {
styleProp: any;
}
/**
* Component representing each 1 hour time block of a calendar
* @param props
*/
function CalendarCell({ styleProp }: Props): JSX.Element {
return (
<div className={styles.calendarCell} style={styleProp}>
<div className={styles.hourLine} />
</div>
);
}
export default CalendarCell;

View File

@@ -1,15 +1,15 @@
import React from 'react'; import React from 'react';
import { Status } from '@shared/types/Course'; import { Status } from '@shared/types/Course';
import Divider from '../Divider/Divider'; import Divider from '../../common/Divider/Divider';
import { Button } from '../Button/Button'; import { Button } from '../../common/Button/Button';
import Text from '../Text/Text'; import Text from '../../common/Text/Text';
import MenuIcon from '~icons/material-symbols/menu'; import MenuIcon from '~icons/material-symbols/menu';
import LogoIcon from '~icons/material-symbols/add-circle-outline';
import UndoIcon from '~icons/material-symbols/undo'; import UndoIcon from '~icons/material-symbols/undo';
import RedoIcon from '~icons/material-symbols/redo'; import RedoIcon from '~icons/material-symbols/redo';
import SettingsIcon from '~icons/material-symbols/settings'; import SettingsIcon from '~icons/material-symbols/settings';
import ScheduleTotalHoursAndCourses from '../ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses'; import ScheduleTotalHoursAndCourses from '../../common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
import CourseStatus from '../CourseStatus/CourseStatus'; import CourseStatus from '../../common/CourseStatus/CourseStatus';
import calIcon from 'src/assets/logo.png';
const CalendarHeader = () => ( const CalendarHeader = () => (
<div className='min-h-79px min-w-672px flex px-0 py-15'> <div className='min-h-79px min-w-672px flex px-0 py-15'>
@@ -18,8 +18,8 @@ const CalendarHeader = () => (
<div className='flex gap-1'> <div className='flex gap-1'>
<Button variant='single' icon={MenuIcon} color='ut-gray' /> <Button variant='single' icon={MenuIcon} color='ut-gray' />
<div className='flex items-center'> <div className='flex items-center'>
<LogoIcon style={{ marginRight: '5px' }} /> <img src={calIcon} className='min-w-[48px] max-w-[48px]' alt='UT Registration Plus Logo' />
<div className='flex flex-col gap-1 whitespace-nowrap'> <div className='flex flex-col whitespace-nowrap'>
<Text className='leading-trim text-cap font-roboto text-base text-ut-burntorange font-medium'> <Text className='leading-trim text-cap font-roboto text-base text-ut-burntorange font-medium'>
UT Registration UT Registration
</Text> </Text>

View File

@@ -1,9 +1,9 @@
import { UserSchedule } from '@shared/types/UserSchedule'; import { UserSchedule } from '@shared/types/UserSchedule';
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import AddSchedule from '~icons/material-symbols/add'; import AddSchedule from '~icons/material-symbols/add';
import List from '../List/List'; import List from '../../common/List/List';
import ScheduleListItem from '../ScheduleListItem/ScheduleListItem'; import ScheduleListItem from '../../common/ScheduleListItem/ScheduleListItem';
import Text from '../Text/Text'; import Text from '../../common/Text/Text';
export type Props = { export type Props = {
style?: React.CSSProperties; style?: React.CSSProperties;
@@ -15,13 +15,8 @@ export function CalendarSchedules(props: Props) {
const [activeScheduleIndex, setActiveScheduleIndex] = useState(props.dummyActiveIndex || 0); const [activeScheduleIndex, setActiveScheduleIndex] = useState(props.dummyActiveIndex || 0);
const [schedules, setSchedules] = useState(props.dummySchedules || []); const [schedules, setSchedules] = useState(props.dummySchedules || []);
let scheduleComponents = schedules.map((schedule, index) => ( const scheduleComponents = schedules.map((schedule, index) => (
<div onClick={() => setActiveScheduleIndex(index)}> <ScheduleListItem active={index === activeScheduleIndex} name={schedule.name} />
<ScheduleListItem
active={index === activeScheduleIndex}
name={schedule.name}
/>
</div>
)); ));
return ( return (
@@ -35,7 +30,7 @@ export function CalendarSchedules(props: Props) {
</div> </div>
</div> </div>
<List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={0} listWidth={240} /> <List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={30} listWidth={240} />
</div> </div>
); );
} }

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Text from './common/Text/Text'; import Text from '../common/Text/Text';
import OutwardArrowIcon from '~icons/material-symbols/arrow-outward'; import OutwardArrowIcon from '~icons/material-symbols/arrow-outward';
type Props = { type Props = {

View File

@@ -1,132 +0,0 @@
import React, { useRef } from 'react';
import * as htmlToImage from 'html-to-image';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
import calIcon from 'src/assets/icons/cal.svg';
import pngIcon from 'src/assets/icons/png.svg';
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
import CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
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 calendarRef = useRef(null); // Create a ref for the calendar grid
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);
});
};
return (
<div className={styles.calendar}>
<div className={styles.dayLabelContainer} />
{/* Displaying the rest of the calendar */}
<div ref={calendarRef} className={styles.timeAndGrid}>
{/* <div className={styles.timeColumn}>
<div className={styles.timeBlock}></div>
{hoursOfDay.map((hour) => (
<div key={hour} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<p>{hour % 12 === 0 ? 12 : hour % 12} {hour < 12 ? 'AM' : 'PM'}</p>
</div>
</div>
))}
</div> */}
<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, index) => (
<React.Fragment key={index}>{row}</React.Fragment>
))}
</div>
</div>
{courseCells.map((block: CalendarGridCourse) => (
<div
key={`${block}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex}`,
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
}}
>
<CalendarCourseCell
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
status={block.componentProps.status}
colors={block.componentProps.colors}
/>
</div>
))}
<div className={styles.buttonContainer}>
<div className={styles.divider} /> {/* First divider */}
<button className={styles.calendarButton}>
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
Save as .CAL
</button>
<div className={styles.divider} /> {/* Second divider */}
<button onClick={saveAsPNG} className={styles.calendarButton}>
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
Save as .PNG
</button>
</div>
</div>
);
}
export default CalendarGrid;

View File

@@ -1,14 +0,0 @@
import React from 'react';
import styles from './CalendarGridCell.module.scss';
/**
* Component representing each 1 hour time block of a calendar
* @param props
*/
const CalendarCell: React.FC = props => (
<div className={styles.calendarCell}>
<div className={styles.hourLine} />
</div>
);
export default CalendarCell;

View File

@@ -1,10 +1,21 @@
import React from 'react'; import React from 'react';
import Text from '../Text/Text'; import Text from '../Text/Text';
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II']; /**
* A type that represents the flags that a course can have.
*/
export type Flag = 'WR' | 'QR' | 'GC' | 'CD' | 'E' | 'II';
export const flagMap: Record<string, Flag> = {
Writing: 'WR',
'Quantitative Reasoning': 'QR',
'Global Cultures': 'GC',
'Cultural Diversity in the United States': 'CD',
Ethics: 'E',
'Independent Inquiry': 'II',
};
interface Props { interface Props {
label: string; label: Flag;
} }
/** /**

View File

@@ -0,0 +1,110 @@
import { Disclosure, Transition } from '@headlessui/react';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import userScheduleHandler from 'src/pages/background/handler/userScheduleHandler';
import DropdownArrowDown from '~icons/material-symbols/arrow-drop-down';
import DropdownArrowUp from '~icons/material-symbols/arrow-drop-up';
import List from '../List/List';
import Text from '../Text/Text';
export type Props = {
style?: React.CSSProperties;
// Dummy value solely for storybook
dummySchedules?: UserSchedule[];
dummyActiveIndex?: number;
dummyActiveSchedule?: UserSchedule;
scheduleComponents?: any[];
};
/**
* This is a reusable dropdown component that can be used to toggle the visiblity of information
*/
export default function Dropdown(props: Props) {
// Expand/Hide state for dropdown
let [expanded, toggle] = React.useState(false);
let [activeScheduleIndex, select] = React.useState(props.dummyActiveIndex);
let [activeSchedule, selectFrom] = React.useState(props.dummyActiveSchedule);
let [scheduleComponents, setScheduleComponents] = React.useState(props.scheduleComponents);
const schedules = props.dummySchedules;
if (schedules == null) {
// if no dummy values passed in
// useSchedules hook here
}
const toggleSwitch = () => {
toggle(!expanded);
};
// WIP function to swap schedules. Prefer to use the hook when in production
const switchSchedule = (index: number) => {
const scheduleToSwitchTo = schedules[index];
select(index);
selectFrom(scheduleToSwitchTo);
if (scheduleToSwitchTo != null && scheduleToSwitchTo.name != null) {
userScheduleHandler.switchSchedule({
data: {
scheduleName: scheduleToSwitchTo.name,
},
sender: null,
sendResponse: null,
});
}
};
return (
<div
style={{ ...props.style, height: expanded && schedules ? `${40 * schedules.length + 54}px` : '72px' }}
className='items-left absolute w-72 flex flex-col border'
>
<Disclosure>
<Disclosure.Button>
<div className='flex items-center border-none bg-white p-3 text-left'>
<div className='flex-1'>
<Text as='div' variant='h4' className='text-ut-burntorange mb-1 w-100%'>
MAIN SCHEDULE:
</Text>
<div>
<Text variant='h3' className='text-theme-black leading-[75%]!'>
{activeSchedule ? activeSchedule.hours : 0} HOURS
</Text>
<Text variant='h4' className='ml-2.5 text-ut-black leading-[75%]!'>
{activeSchedule ? activeSchedule.courses.length : 0} Courses
</Text>
</div>
</div>
<Text className='text-ut-burntorange text-2xl font-normal'>
{expanded ? <DropdownArrowDown /> : <DropdownArrowUp />}
</Text>
</div>
</Disclosure.Button>
<Transition
enter='transition duration-100 ease-out'
enterFrom='transform scale-95 opacity-0'
enterTo='transform scale-100 opacity-100'
leave='transition duration-75 ease-out'
leaveFrom='transform scale-100 opacity-100'
leaveTo='transform scale-95 opacity-0'
beforeEnter={() => {
toggleSwitch();
}}
afterLeave={() => {
toggleSwitch();
}}
>
<Disclosure.Panel>
<List
draggableElements={scheduleComponents}
itemHeight={30}
listHeight={30}
listWidth={240}
gap={10}
/>
</Disclosure.Panel>
</Transition>
</Disclosure>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import React, { ReactElement, useCallback, useState } from 'react'; import type { ReactElement } from 'react';
import React, { useCallback, useState } from 'react';
import { areEqual } from 'react-window'; import { areEqual } from 'react-window';
/* /*

View File

@@ -1,5 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import React, { PropsWithChildren, useCallback } from 'react'; import type { PropsWithChildren } from 'react';
import React, { useCallback } from 'react';
import styles from './Popup.module.scss'; import styles from './Popup.module.scss';
interface Props { interface Props {
@@ -19,8 +21,6 @@ export default function Popup({ onClose, children, className, style, testId, ove
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
const bodyRef = React.useRef<HTMLDivElement>(null); const bodyRef = React.useRef<HTMLDivElement>(null);
const handleClickOutside = useCallback( const handleClickOutside = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
if (!bodyRef.current) return; if (!bodyRef.current) return;

View File

@@ -1,36 +1,38 @@
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import DropdownDrag from '~icons/material-symbols/drag-indicator'; import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
import Text from '../Text/Text'; import Text from '../Text/Text';
export type Props = { export type Props = {
style?: React.CSSProperties; style?: React.CSSProperties;
active?: boolean; active?: boolean;
name: string; name: string;
dragHandleProps?: any;
}; };
/** /**
* This is a reusable dropdown component that can be used to toggle the visiblity of information * This is a reusable dropdown component that can be used to toggle the visiblity of information
*/ */
export default function ScheduleListItem(props: Props) { export default function ScheduleListItem(props: Props) {
const { dragHandleProps } = props;
console.log(props);
return ( return (
<div style={{ ...props.style }} className='items-center'> <div style={{ ...props.style }} className='items-center'>
<li <li className='text-ut-burntorange w-100% flex cursor-pointer items-center self-stretch justify-left'>
className='text-ut-burntorange w-100% flex cursor-pointer items-center self-stretch justify-left'
>
<div className='group flex justify-center'> <div className='group flex justify-center'>
<DropdownDrag className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' /> <div
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
{...dragHandleProps}
>
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
</div>
<div className='inline-flex items-center justify-center gap-1.5'> <div className='inline-flex items-center justify-center gap-1.5'>
<div className='h-5.5 w-5.5 flex items-center justify-center border-2px border-current rounded-full btn-transition group-active:scale-95'> <div className='h-5.5 w-5.5 flex items-center justify-center border-2px border-current rounded-full btn-transition group-active:scale-95'>
<div <div
className={clsx( className={clsx(
'bg-current h-3 w-3 rounded-full transition tansform scale-100 ease-out-expo duration-250', 'bg-current h-3 w-3 rounded-full transition tansform scale-100 ease-out-expo duration-250',
{ {
'scale-0! opacity-0 ease-in-out! duration-200!': !props.active 'scale-0! opacity-0 ease-in-out! duration-200!': !props.active,
} }
)} )}
/> />

View File

@@ -0,0 +1,26 @@
import Popup from '@views/components/common/Popup/Popup';
import React from 'react';
import type { Course } from 'src/shared/types/Course';
import type { UserSchedule } from 'src/shared/types/UserSchedule';
import Description from './Description';
import GradeDistribution from './GradeDistribution';
import HeadingAndActions from './HeadingAndActions';
export interface CourseCatalogInjectedPopupProps {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
}
const CourseCatalogInjectedPopup: React.FC<CourseCatalogInjectedPopupProps> = ({ course, activeSchedule, onClose }) => (
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
<div className='flex flex-col'>
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
<Description lines={course.description} />
<GradeDistribution course={course} />
</div>
</Popup>
);
export default CourseCatalogInjectedPopup;

View File

@@ -0,0 +1,31 @@
import clsx from 'clsx';
import React from 'react';
import Text from '../../common/Text/Text';
interface DescriptionProps {
lines: string[];
}
const Description: React.FC<DescriptionProps> = ({ lines }: DescriptionProps) => {
const keywords = ['prerequisite', 'restricted'];
return (
<ul className='my-[5px] space-y-1.5 children:marker:text-ut-burntorange'>
{lines.map(line => {
const isKeywordPresent = keywords.some(keyword => line.toLowerCase().includes(keyword));
return (
<div className='flex gap-2'>
<span className='text-ut-burntorange'></span>
<li key={line}>
<Text variant='p' className={clsx({ 'font-bold text-ut-burntorange': isKeywordPresent })}>
{line}
</Text>
</li>
</div>
);
})}
</ul>
);
};
export default Description;

View File

@@ -0,0 +1,170 @@
import Spinner from '@views/components/common/Spinner/Spinner';
import Text from '@views/components/common/Text/Text';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import React from 'react';
import type { Course } from 'src/shared/types/Course';
import type { Distribution, LetterGrade } from 'src/shared/types/Distribution';
import { colors } from 'src/shared/util/themeColors';
import {
NoDataError,
queryAggregateDistribution,
querySemesterDistribution,
} from 'src/views/lib/database/queryDistribution';
interface GradeDistributionProps {
course: Course;
}
enum DataStatus {
LOADING = 'LOADING',
FOUND = 'FOUND',
NOT_FOUND = 'NOT_FOUND',
ERROR = 'ERROR',
}
const GRADE_COLORS: Record<LetterGrade, string> = {
A: colors.gradeDistribution.a,
'A-': colors.gradeDistribution.aminus,
'B+': colors.gradeDistribution.bplus,
B: colors.gradeDistribution.b,
'B-': colors.gradeDistribution.bminus,
'C+': colors.gradeDistribution.cplus,
C: colors.gradeDistribution.c,
'C-': colors.gradeDistribution.cminus,
'D+': colors.gradeDistribution.dplus,
D: colors.gradeDistribution.d,
'D-': colors.gradeDistribution.dminus,
F: colors.gradeDistribution.f,
};
const GradeDistribution: React.FC<GradeDistributionProps> = ({ course }) => {
const [semester, setSemester] = React.useState('Aggregate');
const [distributions, setDistributions] = React.useState<Record<string, Distribution>>({});
const [status, setStatus] = React.useState(DataStatus.LOADING);
const ref = React.useRef<HighchartsReact.RefObject>(null);
const chartData = React.useMemo(() => {
if (status === DataStatus.FOUND && distributions[semester]) {
return Object.entries(distributions[semester]).map(([grade, count]) => ({
y: count,
color: GRADE_COLORS[grade as LetterGrade],
}));
}
return [];
}, [distributions, semester, status]);
React.useEffect(() => {
const fetchInitialData = async () => {
try {
const [aggregateDist, semesters] = await queryAggregateDistribution(course);
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
const semesterDistributions = await Promise.all(semesterPromises);
semesters.forEach((semester, i) => {
initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
});
setDistributions(initialDistributions);
setStatus(DataStatus.FOUND);
} catch (e) {
console.error(e);
if (e instanceof NoDataError) {
setStatus(DataStatus.NOT_FOUND);
} else {
setStatus(DataStatus.ERROR);
}
}
};
fetchInitialData();
}, [course]);
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSemester(event.target.value);
};
const chartOptions: Highcharts.Options = {
title: { text: undefined },
subtitle: { text: undefined },
legend: { enabled: false },
xAxis: {
title: { text: 'Grade' },
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'],
crosshair: true,
},
yAxis: {
min: 0,
title: { text: 'Number of Students' },
},
chart: {
style: { fontFamily: 'Roboto Flex', fontWeight: '600' },
spacingBottom: 25,
spacingTop: 25,
height: 250,
},
credits: { enabled: false },
accessibility: { enabled: true },
tooltip: {
headerFormat: '<span style="font-size:small; font-weight:bold">{point.key}</span><table>',
pointFormat:
'<td style="color:{black};padding:0;font-size:small; font-weight:bold;"><b>{point.y:.0f} Students</b></td>',
footerFormat: '</table>',
shared: true,
useHTML: true,
},
plotOptions: {
bar: { pointPadding: 0.2, borderWidth: 0 },
series: { animation: { duration: 700 } },
},
series: [
{
type: 'column',
name: 'Grades',
data: chartData,
},
],
};
return (
<div className='pb-[25px] pt-[12px]'>
{status === DataStatus.LOADING && <Spinner />}
{status === DataStatus.NOT_FOUND && <Text variant='p'>No grade distribution data found</Text>}
{status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
{status === DataStatus.FOUND && (
<>
<div className='w-full flex items-center justify-center gap-[12px]'>
<Text variant='p'>Grade distribution for {`${course.department} ${course.number}`}</Text>
<select
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
onChange={handleSelectSemester}
>
{Object.keys(distributions)
.sort((k1, k2) => {
if (k1 === 'Aggregate') {
return -1;
}
if (k2 === 'Aggregate') {
return 1;
}
const [season1, year1] = k1.split(' ');
const [, year2] = k2.split(' ');
if (year1 !== year2) {
return parseInt(year2, 10) - parseInt(year1, 10);
}
return season1 === 'Fall' ? 1 : -1;
})
.map(semester => (
<option key={semester} value={semester}>
{semester}
</option>
))}
</select>
</div>
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
</>
)}
</div>
);
};
export default GradeDistribution;

View File

@@ -0,0 +1,155 @@
import { Button } from '@views/components/common/Button/Button';
import { Chip, flagMap } from '@views/components/common/Chip/Chip';
import Divider from '@views/components/common/Divider/Divider';
import Text from '@views/components/common/Text/Text';
import React, { useState } from 'react';
import addCourse from 'src/pages/background/lib/addCourse';
import removeCourse from 'src/pages/background/lib/removeCourse';
import type { Course } from 'src/shared/types/Course';
import type { UserSchedule } from 'src/shared/types/UserSchedule';
import { openTabFromContentScript } from 'src/views/lib/openNewTabFromContentScript';
import Add from '~icons/material-symbols/add';
import CalendarMonth from '~icons/material-symbols/calendar-month';
import CloseIcon from '~icons/material-symbols/close';
import Copy from '~icons/material-symbols/content-copy';
import Description from '~icons/material-symbols/description';
import Mood from '~icons/material-symbols/mood';
import Remove from '~icons/material-symbols/remove';
import Reviews from '~icons/material-symbols/reviews';
interface HeadingAndActionProps {
/* The course to display */
course: Course;
/* The active schedule */
activeSchedule: UserSchedule;
/* The function to call when the popup should be closed */
onClose: () => void;
}
export const handleOpenCalendar = async () => {
// Not sure if it's bad practice to export this
const url = chrome.runtime.getURL('calendar.html');
await openTabFromContentScript(url);
};
/**
* Renders the heading component for the CoursePopup component.
*
* @param {HeadingAndActionProps} props - The component props.
* @returns {JSX.Element} The rendered component.
*/
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
const [courseAdded, setCourseAdded] = useState<boolean>(
activeSchedule.courses.some(course => course.uniqueId === uniqueId)
);
const instructorString = instructors
.map(instructor => {
const { firstName, lastName } = instructor;
if (firstName === '') return lastName;
return `${firstName} ${lastName}`;
})
.join(', ');
const handleCopy = () => {
navigator.clipboard.writeText(uniqueId.toString());
};
const handleOpenRateMyProf = async () => {
const openTabs = instructors.map(instructor => {
const { fullName } = instructor;
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${fullName}`;
return openTabFromContentScript(url);
});
await Promise.all(openTabs);
};
const handleOpenCES = async () => {
// TODO: does not look up the professor just takes you to the page
const cisUrl = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580&regl=en-US';
await openTabFromContentScript(cisUrl);
};
const handleOpenPastSyllabi = async () => {
// not specific to professor
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=${courseName}&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`;
await openTabFromContentScript(url);
};
const handleAddOrRemoveCourse = async () => {
if (!courseAdded) {
await addCourse(activeSchedule.name, course);
} else {
await removeCourse(activeSchedule.name, course);
}
setCourseAdded(!courseAdded);
};
return (
<div className='w-full pb-3 pt-6'>
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1'>
<Text variant='h1' className='truncate'>
{courseName}
</Text>
<Text variant='h1' className='flex-1 whitespace-nowrap'>
{' '}
({department} {courseNumber})
</Text>
<Button color='ut-burntorange' variant='single' icon={Copy} onClick={handleCopy}>
{uniqueId}
</Button>
<button className='bg-transparent p-0 btn' onClick={onClose}>
<CloseIcon className='h-7 w-7' />
</button>
</div>
<div className='flex gap-2.5 flex-content-center'>
<Text variant='h4' className='inline-flex items-center justify-center'>
with {instructorString}
</Text>
<div className='flex-content-centr flex gap-1'>
{flags.map(flag => (
<Chip label={flagMap[flag]} />
))}
</div>
</div>
<div className='flex flex-col'>
{schedule.meetings.map(meeting => (
<Text variant='h4'>
{meeting.getDaysString({ format: 'long', separator: 'long' })}{' '}
{meeting.getTimeString({ separator: ' to ', capitalize: false })}
{meeting.location && (
<>
{` in `}
<Text variant='h4' className='text-ut-burntorange underline'>
{meeting.location.building}
</Text>
</>
)}
</Text>
))}
</div>
</div>
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
<Divider type='solid' color='ut-offwhite' className='h-7' />
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
RateMyProf
</Button>
<Button variant='outline' color='ut-teal' icon={Mood} onClick={handleOpenCES}>
CES
</Button>
<Button variant='outline' color='ut-orange' icon={Description} onClick={handleOpenPastSyllabi}>
Past Syllabi
</Button>
<Button
variant='filled'
color={!courseAdded ? 'ut-green' : 'ut-red'}
icon={!courseAdded ? Add : Remove}
onClick={handleAddOrRemoveCourse}
>
{!courseAdded ? 'Add Course' : 'Remove Course'}
</Button>
</div>
<Divider />
</div>
);
};
export default HeadingAndActions;

View File

@@ -1,11 +1,11 @@
import { background } from '@shared/messages'; import { background } from '@shared/messages';
import { Course } from '@shared/types/Course'; import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule'; import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import { Button } from '@views/components/common/Button/Button'; import { Button } from '@views/components/common/Button/Button';
import Card from '@views/components/common/Card/Card'; import Card from '@views/components/common/Card/Card';
import Icon from '@views/components/common/Icon/Icon'; import Icon from '@views/components/common/Icon/Icon';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
import React from 'react';
import styles from './CourseButtons.module.scss'; import styles from './CourseButtons.module.scss';
type Props = { type Props = {
@@ -83,48 +83,43 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
<Button <Button
onClick={openRateMyProfessorURL} onClick={openRateMyProfessorURL}
disabled={!course.instructors.length} disabled={!course.instructors.length}
variant='primary' variant='filled'
className={styles.button} className={styles.button}
color='ut-black'
title='Search for this professor on RateMyProfessor' title='Search for this professor on RateMyProfessor'
> >
<Text /* size='medium' weight='regular' */color='white'> <Text /* size='medium' weight='regular' */ color='white'>RateMyProf</Text>
RateMyProf
</Text>
<Icon className={styles.icon} color='white' name='school' size='medium' /> <Icon className={styles.icon} color='white' name='school' size='medium' />
</Button> </Button>
<Button <Button
onClick={openSyllabiURL} onClick={openSyllabiURL}
variant='secondary' variant='filled'
className={styles.button} className={styles.button}
color='ut-black'
title='Search for syllabi for this course' title='Search for syllabi for this course'
> >
<Text /* size='medium' weight='regular' */ color='white'> <Text /* size='medium' weight='regular' */ color='white'>Syllabi</Text>
Syllabi
</Text>
<Icon className={styles.icon} color='white' name='grading' size='medium' /> <Icon className={styles.icon} color='white' name='grading' size='medium' />
</Button> </Button>
<Button <Button
onClick={openTextbookURL} onClick={openTextbookURL}
variant='tertiary' variant='filled'
className={styles.button} className={styles.button}
color='ut-black'
title='Search for textbooks for this course' title='Search for textbooks for this course'
> >
<Text /* size='medium' weight='regular' color='white' */> <Text /* size='medium' weight='regular' color='white' */>Textbook</Text>
Textbook
</Text>
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' /> <Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
</Button> </Button>
<Button <Button
disabled={!activeSchedule} disabled={!activeSchedule}
onClick={isCourseSaved ? handleRemoveCourse : handleSaveCourse} onClick={isCourseSaved ? handleRemoveCourse : handleSaveCourse}
title={isCourseSaved ? 'Remove this course from your schedule' : 'Add this course to your schedule'} title={isCourseSaved ? 'Remove this course from your schedule' : 'Add this course to your schedule'}
variant={isCourseSaved ? 'danger' : 'success'} variant='filled'
className={styles.button} className={styles.button}
color='ut-black'
> >
<Text /* size='medium' weight='regular' color='white' */>{isCourseSaved ? 'Remove' : 'Add'}</Text>
<Text /* size='medium' weight='regular' color='white' */ >
{isCourseSaved ? 'Remove' : 'Add'}
</Text>
<Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' /> <Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' />
</Button> </Button>
</Card> </Card>

View File

@@ -1,6 +1,7 @@
import { Course } from '@shared/types/Course'; import type { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule'; import type { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react'; import React from 'react';
import Popup from '../../common/Popup/Popup'; import Popup from '../../common/Popup/Popup';
import CourseDescription from './CourseDescription/CourseDescription'; import CourseDescription from './CourseDescription/CourseDescription';
import CourseHeader from './CourseHeader/CourseHeader'; import CourseHeader from './CourseHeader/CourseHeader';

View File

@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { convertMinutesToIndex } from '../useFlattenedCourseSchedule';
describe('useFlattenedCourseSchedule', () => {
it('should convert minutes to index correctly', () => {
const minutes = 480; // 8:00 AM
const expectedIndex = 2; // (480 - 420) / 30 = 2
const result = convertMinutesToIndex(minutes);
expect(result).toBe(expectedIndex);
});
});

View File

@@ -1,4 +1,6 @@
import { CalendarCourseCellProps } from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell'; import type { CalendarCourseCellProps } from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import type { CourseCatalogInjectedPopupProps } from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import useSchedules from './useSchedules'; import useSchedules from './useSchedules';
const dayToNumber: { [day: string]: number } = { const dayToNumber: { [day: string]: number } = {
@@ -21,9 +23,13 @@ interface CalendarGridPoint {
export interface CalendarGridCourse { export interface CalendarGridCourse {
calendarGridPoint: CalendarGridPoint; calendarGridPoint: CalendarGridPoint;
componentProps: CalendarCourseCellProps; componentProps: CalendarCourseCellProps;
gridColumnStart?: number;
gridColumnEnd?: number;
totalColumns?: number;
popupProps: CourseCatalogInjectedPopupProps;
} }
const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30); export const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30);
/** /**
* Get the active schedule, and convert it to be render-able into a calendar. * Get the active schedule, and convert it to be render-able into a calendar.
@@ -54,6 +60,7 @@ export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
}, },
componentProps: { componentProps: {
courseDeptAndInstr, courseDeptAndInstr,
timeAndLocation: 'Asynchronous',
status, status,
colors: { colors: {
// TODO: figure out colors - these are defaults // TODO: figure out colors - these are defaults
@@ -61,6 +68,11 @@ export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
secondaryColor: 'ut-gray', secondaryColor: 'ut-gray',
}, },
}, },
popupProps: {
course,
activeSchedule,
onClose: () => {},
},
}, },
]; ];
} }
@@ -87,6 +99,11 @@ export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
secondaryColor: 'ut-orange', secondaryColor: 'ut-orange',
}, },
}, },
popupProps: {
course,
activeSchedule,
onClose: () => {}, // Add onClose property here
},
})); }));
}); });
}) })

View File

@@ -0,0 +1,25 @@
import { createMessenger } from "chrome-extension-toolkit";
type MyMessages = {
openNewTab: {
data: { url: string };
};
};
const messenger = createMessenger<MyMessages>('background');
/**
* Content scripts and background scripts are isolated environments.
* Content scripts are where our code interacting with the webpage lives,
* whereas the background script is where we can open a tab from.
* This function allows us to open a new tab from the content script by communicating
* with the background script.
*/
export async function openTabFromContentScript(url: string) {
// @ts-ignore
messenger.openNewTab({ url }).then(() => {
console.log('New tab opened with URL:', url);
}).catch((error) => {
console.error('Error opening new tab:', error);
});
}

9
vitest.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
},
},
});