Compare commits

...

34 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
DhruvArora-03
70c75ff481 add button to the rows, use new ConflictsWithWarning component 2024-02-19 11:29:24 -06:00
DhruvArora-03
7f76af7ab3 fix ConflictsWithWarning 2024-02-19 11:28:33 -06:00
DhruvArora-03
a538f20aad Merge remote-tracking branch 'origin/hackathon' into feat-conflict-row 2024-02-19 11:11:37 -06:00
Lukas Zenick
0acd0b722c html2canvas -> htmlToImage
also fixed derick's bugs
2024-02-18 12:46:35 -06:00
ae32d0b645 feat: Derek/export png (#95)
* Attempting to use more lightweight version

* Did not work.

* This is not what I wanted

* The image saves correctly. Needs padding

* Padding !!

* Removed downloadjs

* Padding more
2024-02-17 19:09:41 -06:00
Casey Charleston
51b6799d73 dropdown and calendar schedules storybook 2024-02-17 18:35:17 -06:00
DhruvArora-03
f214ed7c01 Merge remote-tracking branch 'origin/hackathon' into feat-conflict-row 2024-02-17 18:21:39 -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
Lukas Zenick
e44b0c0e45 FIX README 2024-02-17 17:24:27 -06:00
Lukas Zenick
206c97c5b5 fixed README 2024-02-17 17:23:04 -06:00
vivek12311
ff4ee494b6 Using React-icons. Probably need to take out later 2024-02-17 16:03:28 -06:00
82 changed files with 3877 additions and 1092 deletions

107
.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,26 +1,26 @@
name: "Chromatic" name: 'Chromatic'
on: [push, pull_request_target] on: [push, pull_request]
jobs: jobs:
chromatic: chromatic:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 8 version: 8
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Publish to Chromatic - name: Publish to Chromatic
uses: chromaui/action@latest uses: chromaui/action@latest
with: with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true exitZeroOnChanges: true
autoAcceptChanges: "main" autoAcceptChanges: 'main'

24
.github/workflows/tests.yml vendored Normal file
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"
}
} }

View File

@@ -2,17 +2,29 @@
## Built Using ## Built Using
- React 18 - React 18
- TypeScript - TypeScript
- Vite 5 - Vite 5
- ESLint - ESLint
- Prettier - Prettier
- Semantic-Release - Semantic-Release
- Custom Messaging & Storage Wrappers - Custom Messaging & Storage Wrappers
## Getting Started ## Getting Started
1. Clone this repo 1. Clone this repo
2. Run `pnpm install` to install and patch all the required dependencies 2. Run `pnpm install` to install and patch all the required dependencies
3. Run `pnpm run dev` to start the development server
4. Run `pnpm build` to build the extension for production - If you want to run the development build:
- Run `pnpm run dev`
- If you want to build the extension for production:
- Run `pnpm build`
You may have to rename the `__uno.css.js` to `uno.css.js` in dist
Go to chrome://extensions, ensure you have "Developer Mode" enabled, and click 'Load unpacked'
Navigate to the 'dist' folder and click 'select' to import the extension

123
commitlint.config.ts Normal file
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",
@@ -24,34 +33,36 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"highcharts": "^11.3.0", "highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1", "highcharts-react-official": "^3.2.1",
"html2canvas": "^1.4.1", "html-to-image": "^1.11.11",
"react": "^18.2.0", "react": "^18.2.0",
"react-devtools-core": "^5.0.0", "react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"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": {

2080
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,13 +7,18 @@ import { Course } from './Course';
export class UserSchedule { export class UserSchedule {
courses: Course[]; courses: Course[];
name: string; name: string;
hours: number;
constructor(schedule: Serialized<UserSchedule>) { constructor(schedule: Serialized<UserSchedule>) {
this.courses = schedule.courses.map(c => new Course(c)); this.courses = schedule.courses.map(c => new Course(c));
this.name = schedule.name; this.name = schedule.name;
this.hours = 0;
for (const course of this.courses) {
this.hours += course.creditHours;
}
} }
containsCourse(course: Course): boolean { containsCourse(course: Course): boolean {
return this.courses.some(c => c.uniqueId === course.uniqueId); return this.courses.some(c => c.uniqueId === course.uniqueId);
} }
} }

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

@@ -1,5 +1,96 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning'; import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import Instructor from 'src/shared/types/Instructor';
export const ExampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
description: [
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
'May be counted toward the Quantitative Reasoning flag requirement.',
'Designed to accommodate 100 or more students.',
'Taught as a Web-based course.',
],
flags: ['Quantitative Reasoning'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'Bevo',
lastName: 'Bevo',
fullName: 'Bevo Bevo',
}),
],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 12345,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
export const ExampleCourse2: Course = new Course({
courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
creditHours: 3,
department: 'C S',
description: [
'Restricted to computer science majors.',
'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
'Computer Science 439 and 439H may not both be counted.',
'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
'May be counted toward the Independent Inquiry flag requirement.',
],
flags: ['Independent Inquiry'],
fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
instructionMode: 'In Person',
instructors: [
new Instructor({
firstName: 'Allison',
lastName: 'Norman',
fullName: 'Allison Norman',
}),
],
isReserved: false,
number: '439',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
startTime: 930,
endTime: 1050,
}),
new CourseMeeting({
days: ['Friday'],
startTime: 600,
endTime: 720,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 67890,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
const meta = { const meta = {
title: 'Components/Common/ConflictsWithWarning', title: 'Components/Common/ConflictsWithWarning',
@@ -9,8 +100,10 @@ const meta = {
}, },
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
ConflictingCourse: { control: 'string' }, conflicts: { control: 'object' },
SectionNumber: { control: 'string' }, },
args: {
conflicts: [ExampleCourse, ExampleCourse2],
}, },
} satisfies Meta<typeof ConflictsWithWarning>; } satisfies Meta<typeof ConflictsWithWarning>;
export default meta; export default meta;
@@ -19,7 +112,6 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: { args: {
ConflictingCourse: 'BVO 311C', conflicts: [ExampleCourse, ExampleCourse2],
SectionNumber: '47280',
}, },
}; };

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' }}>
Clear Courses UT Registration
</Button> </Text>
<Text as='div' variant='h1-course' style={{ color: '#f8971f', fontSize: '1.3rem' }}>
Plus
</Text>
</div>
</div>
<div className='flex items-center'>
<button
style={{ backgroundColor: '#bf5700', borderRadius: '8px', padding: '8px' }}
onClick={handleOpenCalendar}
>
{/* <FaCalendarAlt color='white' /> */}
</button>
<button
style={{
backgroundColor: 'white',
marginLeft: '10px',
borderRadius: '8px',
padding: '8px',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
}}
onClick={handleOpenOptions}
>
{/* <FaCog color='#C05621' /> */}
</button>
</div>
</div>
<Divider color='#E2E8F0' type='solid' style={{ margin: '1rem 0' }} />
<div
className='mb-4 rounded-lg bg-white p-2 text-left shadow-inner'
style={{ backgroundColor: 'white', border: '1px solid #FBD38D', borderRadius: '0.5rem' }}
>
<Text as='div' variant='h2-course' style={{ color: '#DD6B20', fontSize: '1.2rem' }}>
MAIN SCHEDULE:
</Text>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'start', color: '#333f48' }}>
<Text
as='div'
variant='h1'
style={{ fontSize: '1.2rem', fontWeight: 'bold', marginRight: '0.5rem' }}
>
22 HOURS
</Text>
<Text as='div' variant='h2-course' style={{ fontSize: '1.2rem' }}>
8 Courses
</Text>
</div>
</div>
{/* Integrate the List component here */}
{activeSchedule ? (
<List
draggableElements={draggableElements}
itemHeight={100} // Adjust based on your content size
listHeight={500} // Adjust based on total height you want for the list
listWidth={350} // Adjust based on your layout/design
gap={12} // Spacing between items
/>
) : null}
<div className='mt-4 flex justify-between border-t border-gray-200 p-4 text-xs'>
<div className='flex items-center'>
<div
style={{
backgroundColor: '#6B7280',
padding: '1px',
borderRadius: '4px',
marginRight: '3px',
marginLeft: '8px',
}}
>
<StatusIcon status={Status.WAITLISTED} className='h-5 w-5 text-white' />
</div>
<Text as='span' variant='mini'>
WAITLISTED
</Text>
</div>
<div className='flex items-center'>
<div
style={{
backgroundColor: '#6B7280',
padding: '1px',
borderRadius: '4px',
marginRight: '3px',
marginLeft: '8px',
}}
>
<StatusIcon status={Status.CLOSED} className='h-5 w-5 text-white' />
</div>
<Text as='span' variant='mini'>
CLOSED
</Text>
</div>
<div className='flex items-center'>
<div
style={{
backgroundColor: '#6B7280',
padding: '1px',
borderRadius: '4px',
marginRight: '3px',
marginLeft: '8px',
}}
>
<StatusIcon status={Status.CANCELLED} className='h-5 w-5 text-white' />
</div>
<Text as='span' variant='mini'>
CANCELLED
</Text>
</div>
</div>
<div className='mt-2 text-center text-xs'>
<div className='inline-flex items-center justify-center'>
<Text as='div' variant='mini'>
DATA UPDATED ON: 12:00 AM 02/01/2024
</Text>
{/* <FaRedo className='ml-2 h-4 w-4 text-gray-600' /> */}
</div>
</div>
</div>
</ExtensionRoot> </ExtensionRoot>
); );
} }

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 {
@@ -109,7 +113,7 @@
background-color: transparent; background-color: transparent;
color: #333; color: #333;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
} }
@@ -124,4 +128,13 @@
height: 30px; height: 30px;
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,111 +0,0 @@
import React, { useRef } from 'react';
import html2canvas from 'html2canvas';
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 = () => {
if (calendarRef.current) {
html2canvas(calendarRef.current).then(canvas => {
// Create an a element to trigger download
const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'calendar.png';
a.click();
});
}
};
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

@@ -1,43 +1,37 @@
import React from 'react'; import React from 'react';
import { Course } from 'src/shared/types/Course';
import clsx from 'clsx';
import Text from '../Text/Text'; import Text from '../Text/Text';
/** /**
* Props for ConflictWithWarningProps * Props for ConflictWithWarningProps
*/ */
export interface ConflictsWithWarningProps { export interface ConflictsWithWarningProps {
ConflictingCourse: string; className?: string;
SectionNumber: string; conflicts: Course[];
} }
/** /**
* The ConflictsWithWarning component is used to display a warning message when a course conflicts * The ConflictsWithWarning component is used to display a warning message when a course conflicts
* with another course as part of the labels and details section * with another course as part of the labels and details section
* *
* @param props ConflictsWithWarningProps * @param props ConflictsWithWarningProps
*/ */
export default function ConflictsWithWarning( { ConflictingCourse, SectionNumber }: ConflictsWithWarningProps): JSX.Element { export default function ConflictsWithWarning({ className, conflicts }: ConflictsWithWarningProps): JSX.Element {
const UniqueCourseConflictText = `${ConflictingCourse} (${SectionNumber})`;
return (
<div className="min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5">
<ConflictsWithoutWarningText>
Conflicts With:
</ConflictsWithoutWarningText>
<ConflictsWithoutWarningText>
{UniqueCourseConflictText}
</ConflictsWithoutWarningText>
</div>
);
}
function ConflictsWithoutWarningText( {children}: {children: string} ) {
return ( return (
<Text <Text
variant='mini' variant='mini'
as='span' className={clsx(
className='text-white' className,
'min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5 text-white'
)}
> >
{children} <div>Conflicts With:</div>
{conflicts.map(course => (
<div>
{`${course.department} ${course.number} (${course.uniqueId})`}
</div>
))}
</Text> </Text>
); );
} }

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

@@ -3,9 +3,9 @@ import { UserSchedule } from '@shared/types/UserSchedule';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Button } from '../../common/Button/Button'; import { Button } from '../../common/Button/Button';
import Icon from '../../common/Icon/Icon';
import Text from '../../common/Text/Text';
import styles from './TableRow.module.scss'; import styles from './TableRow.module.scss';
import ConflictsWithWarning from '../../common/ConflictsWithWarning/ConflictsWithWarning';
import AddIcon from '~icons/material-symbols/add-circle';
interface Props { interface Props {
isSelected: boolean; isSelected: boolean;
@@ -54,7 +54,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
return () => { return () => {
element.classList.remove(styles.inActiveSchedule); element.classList.remove(styles.inActiveSchedule);
}; };
}, [activeSchedule, element.classList]); }, [activeSchedule, course, element.classList]);
useEffect(() => { useEffect(() => {
if (!activeSchedule || !course) { if (!activeSchedule || !course) {
@@ -84,20 +84,14 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
return ReactDOM.createPortal( return ReactDOM.createPortal(
<> <>
<Button className={styles.rowButton} onClick={onClick} variant='secondary'> <Button
<Icon name='bar_chart' color='white' size='medium' /> icon={AddIcon}
</Button> className={styles.rowButton}
{conflicts.length > 0 && ( color='ut-burntorange'
<div className={styles.conflictTooltip}> onClick={onClick}
<div className={styles.body}> variant='single'
{conflicts.map(c => ( />
<Text /* size='small' */ key={c.uniqueId}> {conflicts.length > 0 && <ConflictsWithWarning className={styles.conflictTooltip} conflicts={conflicts} />}
{c.department} {c.number} ({c.uniqueId})
</Text>
))}
</div>
</div>
)}
</>, </>,
container container
); );

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