diff --git a/package-lock.json b/package-lock.json index b90e3fa..37dbe9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@plasmohq/messaging": "^0.1.6", + "@plasmohq/storage": "^1.3.1", "apexcharts": "^3.37.1", "next": "^13.1.5", "plasmo": "^0.67.3", @@ -16,6 +17,7 @@ "react-apexcharts": "^1.4.0", "react-dom": "18.2.0", "react-icons": "^4.7.1", + "react-loader-spinner": "^5.3.4", "react-router-dom": "^6.8.2" }, "devDependencies": { @@ -117,6 +119,17 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", @@ -499,6 +512,29 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "dependencies": { + "@emotion/memoize": "^0.8.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@esbuild/android-arm": { "version": "0.17.11", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", @@ -3058,6 +3094,33 @@ "prettier": "2.x" } }, + "node_modules/@plasmohq/storage": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@plasmohq/storage/-/storage-1.3.1.tgz", + "integrity": "sha512-K+EsksYrpBBnI3ZgWhNKJoKfjCzyEOrhZOC6oxzU9p7iKhvpQOIELY8JlFmc+sUnof+Ta6XwMvLJnxhrlcXt8w==", + "dependencies": { + "pify": "6.1.0" + }, + "peerDependencies": { + "react": "^16.8.6 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@plasmohq/storage/node_modules/pify": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-6.1.0.tgz", + "integrity": "sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", @@ -3725,6 +3788,26 @@ "postcss": "^8.1.0" } }, + "node_modules/babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3952,6 +4035,14 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001466", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001466.tgz", @@ -4300,6 +4391,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -4315,6 +4414,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -5049,6 +5158,14 @@ "tslib": "^2.0.3" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/htmlnano": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.0.3.tgz", @@ -7073,6 +7190,25 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-loader-spinner": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz", + "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-refresh": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", @@ -7403,6 +7539,11 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/sharp": { "version": "0.31.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz", @@ -7667,6 +7808,35 @@ "node": ">=0.10.0" } }, + "node_modules/styled-components": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.9.tgz", + "integrity": "sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A==", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -7689,6 +7859,11 @@ } } }, + "node_modules/styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==" + }, "node_modules/sucrase": { "version": "3.29.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.29.0.tgz", @@ -8509,6 +8684,14 @@ } } }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, "@babel/helper-compilation-targets": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", @@ -8792,6 +8975,29 @@ "to-fast-properties": "^2.0.0" } }, + "@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "requires": { + "@emotion/memoize": "^0.8.0" + } + }, + "@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@esbuild/android-arm": { "version": "0.17.11", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", @@ -10292,6 +10498,21 @@ "lodash.isequal": "4.5.0" } }, + "@plasmohq/storage": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@plasmohq/storage/-/storage-1.3.1.tgz", + "integrity": "sha512-K+EsksYrpBBnI3ZgWhNKJoKfjCzyEOrhZOC6oxzU9p7iKhvpQOIELY8JlFmc+sUnof+Ta6XwMvLJnxhrlcXt8w==", + "requires": { + "pify": "6.1.0" + }, + "dependencies": { + "pify": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-6.1.0.tgz", + "integrity": "sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==" + } + } + }, "@pnpm/network.ca-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", @@ -10765,6 +10986,23 @@ "postcss-value-parser": "^4.2.0" } }, + "babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10912,6 +11150,11 @@ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-lite": { "version": "1.0.30001466", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001466.tgz", @@ -11174,6 +11417,11 @@ } } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, "css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -11186,6 +11434,16 @@ "nth-check": "^2.0.1" } }, + "css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -11710,6 +11968,14 @@ "tslib": "^2.0.3" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "htmlnano": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.0.3.tgz", @@ -13069,6 +13335,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-loader-spinner": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz", + "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==", + "requires": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-refresh": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", @@ -13294,6 +13577,11 @@ "upper-case-first": "^2.0.2" } }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "sharp": { "version": "0.31.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz", @@ -13474,6 +13762,23 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" }, + "styled-components": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.9.tgz", + "integrity": "sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + } + }, "styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -13482,6 +13787,11 @@ "client-only": "0.0.1" } }, + "styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==" + }, "sucrase": { "version": "3.29.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.29.0.tgz", diff --git a/package.json b/package.json index ca6ba00..6683711 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sk.edge", "displayName": "sk.edge", "version": "0.0.1", - "description": "the all-in-one registration tool by students, for students", + "description": "your registration assistant by students, for students", "author": "Nebula Labs", "packageManager": "npm@8.19.2", "scripts": { @@ -12,6 +12,7 @@ }, "dependencies": { "@plasmohq/messaging": "^0.1.6", + "@plasmohq/storage": "^1.3.1", "apexcharts": "^3.37.1", "next": "^13.1.5", "plasmo": "^0.67.3", @@ -19,6 +20,7 @@ "react-apexcharts": "^1.4.0", "react-dom": "18.2.0", "react-icons": "^4.7.1", + "react-loader-spinner": "^5.3.4", "react-router-dom": "^6.8.2" }, "devDependencies": { diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..1b3cf20 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,73 @@ +import { scrapeCourseData, CourseHeader } from "~content"; +import { Storage } from "@plasmohq/storage"; + +export interface ShowCourseTabPayload { + header: CourseHeader; + professors: string[]; +} + +// State vars +let courseTabId: number = null; +let scrapedCourseData: ShowCourseTabPayload = null; + +// for persistent state +const storage = new Storage(); + +/** Injects the content script if we hit a course page */ +chrome.webNavigation.onHistoryStateUpdated.addListener(details => { + if (/^.*:\/\/utdallas\.collegescheduler\.com\/terms\/.*\/courses\/.+$/.test( + details.url + )) + { + chrome.scripting.executeScript({ + target: { + tabId: details.tabId, + }, + world: "MAIN", + // content script injection only works reliably on the prod packaged extension + // b/c of the plasmo dev server connections + func: scrapeCourseData, + }, async function (resolve) { + if (resolve && resolve[0] && resolve[0].result) { + const result: ShowCourseTabPayload = resolve[0].result; + scrapedCourseData = result; + await storage.set("scrapedCourseData", scrapedCourseData) + }; + }); + chrome.action.setBadgeText({text: "!"}); + chrome.action.setBadgeBackgroundColor({color: 'green'}); + courseTabId = details.tabId + storage.set("courseTabId", courseTabId) + storage.set("courseTabUrl", details.url) + } else { + chrome.action.setBadgeText({text: ""}); + } +}); + +/** Sets the icon to be active if we're on a course tab */ +chrome.tabs.onActivated.addListener(async details => { + const cachedTabUrl: string = await storage.get("courseTabUrl") + const currentTabUrl: string = (await getCurrentTab()).url + if (cachedTabUrl === currentTabUrl) { + chrome.action.setBadgeText({text: "!"}); + chrome.action.setBadgeBackgroundColor({color: 'green'}); + } else { + chrome.action.setBadgeText({text: ""}); + } +}); + +export async function getScrapedCourseData() { + const cachedTabUrl: string = await storage.get("courseTabUrl") + const currentTabUrl: string = (await getCurrentTab()).url + if (cachedTabUrl === currentTabUrl) { + return await storage.get("scrapedCourseData"); + } + return null +} + +async function getCurrentTab() { + let queryOptions = { active: true, lastFocusedWindow: true }; + // `tab` will either be a `tabs.Tab` instance or `undefined`. + let [tab] = await chrome.tabs.query(queryOptions); + return tab; +} \ No newline at end of file diff --git a/src/background/index.ts b/src/background/index.ts deleted file mode 100644 index 17fe89b..0000000 --- a/src/background/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import contentFile from 'url:./content.ts'; -import type { CourseHeader, ShowCourseTabPayload } from "../backgroundInterfaces"; -import ScrapeCourseData from "./content"; -import { redirect } from "react-router-dom"; - -let courseTabId: number = null; -let scrapedCourseData: ShowCourseTabPayload = null; - -const messageType = { - SHOW_COURSE_TAB: "SHOW_COURSE_TAB", - SHOW_PROFESSOR_TAB: "SHOW_PROFESSOR_TAB", - REQUEST_PROFESSORS: "REQUEST_PROFESSORS", - GET_NEBULA_PROFESSOR: "GET_NEBULA_PROFESSOR", - GET_NEBULA_COURSE: "GET_NEBULA_COURSE", - GET_NEBULA_SECTIONS: "GET_NEBULA_SECTIONS" -}; - -/** Injects the content script if we hit a course page */ -chrome.webNavigation.onHistoryStateUpdated.addListener(details => { - if (/^.*:\/\/utdallas\.collegescheduler\.com\/terms\/.*\/courses\/.+$/.test( - details.url - )) - { - chrome.scripting.executeScript({ - target: { - tabId: details.tabId, - }, - world: "MAIN", - // below is a gigamega hack from https://github.com/PlasmoHQ/plasmo/issues/150 - // content script injection only works reliably on the prod packaged extension - // b/c of the plasmo dev server connections - func: ScrapeCourseData, - }, function (resolve) { - if (resolve && resolve[0] && resolve[0].result) { - const result: ShowCourseTabPayload = resolve[0].result; - - // Now let's save this scraped value. - scrapedCourseData = result; - }; - }); - chrome.action.setBadgeText({text: "!"}); - chrome.action.setBadgeBackgroundColor({color: 'green'}); - courseTabId = details.tabId - } else { - chrome.action.setBadgeText({text: ""}); - } -}); - -/** Sets the icon to be active if we're on a course tab */ -chrome.tabs.onActivated.addListener(details => { - if (details.tabId == courseTabId) { - chrome.action.setBadgeText({text: "!"}); - chrome.action.setBadgeBackgroundColor({color: 'green'}); - } else { - chrome.action.setBadgeText({text: ""}); - } -}); - -export function getScrapedCourseData() { - return scrapedCourseData; -} \ No newline at end of file diff --git a/src/background/messages/getScrapeData.ts b/src/background/messages/getScrapeData.ts index 5c950d4..d83e10e 100644 --- a/src/background/messages/getScrapeData.ts +++ b/src/background/messages/getScrapeData.ts @@ -1,12 +1,9 @@ import type { PlasmoMessaging } from "@plasmohq/messaging" -import {getScrapedCourseData} from "../index"; +import { getScrapedCourseData } from "../../background"; const handler: PlasmoMessaging.MessageHandler = async (req, res) => { - const data = getScrapedCourseData(); - - res.send({ - data - }) + const data = await getScrapedCourseData(); + res.send(data) } export default handler \ No newline at end of file diff --git a/src/backgroundInterfaces.ts b/src/backgroundInterfaces.ts deleted file mode 100644 index be9a2e9..0000000 --- a/src/backgroundInterfaces.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface CourseHeader { - subjectPrefix: string; - courseNumber: string; -} - -export interface ShowCourseTabPayload { - courseData: CourseHeader; - professors: string[]; -} \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..8aa3f36 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,17 @@ +import nebulaLogo from "data-base64:../../assets/nebula-logo.svg" + +export const Footer = () => { + + const navigateToNebula = (): void => { + window.open("https://www.utdnebula.com/", "_blank") + } + + return ( +
+
+

Powered by Nebula Labs

+ Nebula Labs Logo +
+
+ ) +} \ No newline at end of file diff --git a/src/components/HorizontalScores.tsx b/src/components/HorizontalScores.tsx index 3eea218..3b4cd59 100644 --- a/src/components/HorizontalScores.tsx +++ b/src/components/HorizontalScores.tsx @@ -12,9 +12,9 @@ export const HorizontalScores = ({ rmpScore, diffScore, wtaPercent } : Scores) =

RMP

DIFF

WTA

-

{rmpScore}

-

{diffScore}

-

{wtaPercent}%

+

{rmpScore ? rmpScore.toFixed(1) : "NA"}

+

{diffScore ? diffScore.toFixed(1) : "NA"}

+

{wtaPercent ? Math.round(wtaPercent) : "NA"}%

) } \ No newline at end of file diff --git a/src/components/Landing.tsx b/src/components/Landing.tsx new file mode 100644 index 0000000..a1f72b6 --- /dev/null +++ b/src/components/Landing.tsx @@ -0,0 +1,33 @@ +import { Card } from "./Card" +import { Footer } from "./Footer" +import skedgeLogo from "data-base64:../../assets/icon.png" + +const SURVEY_URL = "https://docs.google.com/forms/d/e/1FAIpQLScGIXzlYgsx1SxHYTTCwRaMNVYNRe6I67RingPRVzcT1tLwSg/viewform?usp=sf_link" +const GALAXY_URL = "https://www.utdallas.edu/galaxy/" + +export const Landing = () => { + + const navigativeToScheduler = (): void => { + window.open(GALAXY_URL, "_blank") + } + + const navigateToSurvey = (): void => { + window.open(SURVEY_URL, "_blank") + } + + return( + +
+

Welcome to sk.edge 👋

+
your registration assistant by students, for students
+ +

Log into Schedule Planner and click Options on a course to get started!

+

Got feedback? Let us know !

+ +
+
+ ) +} \ No newline at end of file diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx new file mode 100644 index 0000000..ac9d8c3 --- /dev/null +++ b/src/components/Loading.tsx @@ -0,0 +1,18 @@ +import { Rings } from "react-loader-spinner" + +export const Loading = () => { + return ( +
+ +
+ ) +} diff --git a/src/components/MiniGrades.tsx b/src/components/MiniGrades.tsx index bc6ad23..832d707 100644 --- a/src/components/MiniGrades.tsx +++ b/src/components/MiniGrades.tsx @@ -1,12 +1,14 @@ +import { useState } from "react"; import Chart from "react-apexcharts" import { miniGradeChartOptions } from "~utils/styling"; import type { GradeDistribution } from "./ProfileGrades" export const MiniGrades = ({ gradeDistributionData } : { gradeDistributionData: GradeDistribution }) => { - miniGradeChartOptions.title.text = gradeDistributionData.name; + const config = JSON.parse(JSON.stringify(miniGradeChartOptions)) + config.title.text = gradeDistributionData.name; return ( <> - + ) } \ No newline at end of file diff --git a/src/components/MiniProfessor.tsx b/src/components/MiniProfessor.tsx index 86ee356..7a22ca0 100644 --- a/src/components/MiniProfessor.tsx +++ b/src/components/MiniProfessor.tsx @@ -1,15 +1,15 @@ import { FaUser } from "react-icons/fa" import { NavigateFunction, useNavigate } from "react-router-dom" -import type { ProfessorProfileInterface } from "~routes/about" +import type { ProfessorProfileInterface } from "~data/builder" import { Card } from "./Card" import { MiniGrades } from "./MiniGrades" import { MiniScore } from "./MiniScore" -export const MiniProfessor = ({ professorData } : { professorData: ProfessorProfileInterface }) => { +export const MiniProfessor = ({ professorData, profiles } : { professorData: ProfessorProfileInterface, profiles: ProfessorProfileInterface[] }) => { const navigation: NavigateFunction = useNavigate() const toProfessorProfile = (): void => { - navigation("/about", { state: professorData }) + navigation("/professor", { state: { professorData, profiles } }) } return( diff --git a/src/components/MiniScore.tsx b/src/components/MiniScore.tsx index 4ee2e8b..9f8d40b 100644 --- a/src/components/MiniScore.tsx +++ b/src/components/MiniScore.tsx @@ -12,6 +12,7 @@ export const MiniScore = ({ name, score, maxScore, inverted } : MiniScoreProps) return(

{name}

-

{name == "WTA" ? score : score.toFixed(1)}

+ {score !== undefined &&

{name === "WTA" ? Math.round(score) : score.toFixed(1)}

} + {score === undefined &&

NA

}
)} \ No newline at end of file diff --git a/src/components/ProfileGrades.tsx b/src/components/ProfileGrades.tsx index 402588c..007193f 100644 --- a/src/components/ProfileGrades.tsx +++ b/src/components/ProfileGrades.tsx @@ -29,10 +29,10 @@ export const ProfileGrades = ({ gradeDistributionData } : { gradeDistributionDat return ( <> -
- -

{gradeDistributionData[page].name}

- +
+ +

{gradeDistributionData[page].name}

+
diff --git a/src/components/ProfileHeader.tsx b/src/components/ProfileHeader.tsx index 0d16387..2ee6622 100644 --- a/src/components/ProfileHeader.tsx +++ b/src/components/ProfileHeader.tsx @@ -1,12 +1,17 @@ import { TiArrowBack } from "react-icons/ti" import { FaExternalLinkAlt } from "react-icons/fa" import { NavigateFunction, useNavigate } from "react-router-dom" +import type { ProfessorProfileInterface } from "~data/builder"; -export const ProfileHeader = ({ name, profilePicUrl } : { name: string, profilePicUrl: string }) => { +export const ProfileHeader = ({ name, profilePicUrl, rmpId, profiles } : { name: string, profilePicUrl: string, rmpId: number, profiles: ProfessorProfileInterface[] }) => { const navigation: NavigateFunction = useNavigate(); const returnToSections = (): void => { - navigation(-1) + navigation("/", { state: profiles }) + } + + const navigativeToRmp = (): void => { + window.open('https://www.ratemyprofessors.com/professor/' + rmpId, '_blank') } return ( @@ -15,13 +20,13 @@ export const ProfileHeader = ({ name, profilePicUrl } : { name: string, profileP -

{name}

-
- +
) diff --git a/src/background/content.ts b/src/content.ts similarity index 86% rename from src/background/content.ts rename to src/content.ts index 1ac064d..cd27a0f 100644 --- a/src/background/content.ts +++ b/src/content.ts @@ -1,23 +1,23 @@ +export interface CourseHeader { + subjectPrefix: string; + courseNumber: string; +} + +// Plasmo CS config export +export const config = { + matches: ["https://utdallas.collegescheduler.com/terms/*/courses/*"] +} + /** * This script runs when we select a course in the Scheduler * - It scrapes the page for course data * - It scrapes the names of instructors * - It injects the instructor names into the section table */ -import type { CourseHeader } from "../backgroundInterfaces"; - -export default async function ScrapeCourseData() { - const config = { - matches: ["https://utdallas.collegescheduler.com/terms/*/courses/*"] - } - - var messageType = { - SHOW_COURSE_TAB: 'SHOW_COURSE_TAB', - SHOW_PROFESSOR_TAB: 'SHOW_PROFESSOR_TAB', - } +export async function scrapeCourseData() { - let [courseData, professors] = await Promise.all([getCourseInfo(), injectAndGetProfessorNames()]); - return {courseData: courseData, professors: professors}; + let [ header, professors ] = await Promise.all([getCourseInfo(), injectAndGetProfessorNames()]); + return { header: header, professors: professors }; /** Gets the first element from the DOM specified by selector */ function waitForElement(selector: string): Promise { diff --git a/src/data/builder.ts b/src/data/builder.ts new file mode 100644 index 0000000..a00f8ab --- /dev/null +++ b/src/data/builder.ts @@ -0,0 +1,62 @@ +import type { ShowCourseTabPayload } from "~background"; +import { fetchNebulaCourse, fetchNebulaProfessor, fetchNebulaSections } from "./fetch"; +import { requestProfessorsFromRmp } from "~data/fetchFromRmp"; +import { SCHOOL_ID } from "./config"; + +export interface ProfessorProfileInterface { + name: string; + profilePicUrl: string; + rmpId: number; + rmpScore: number; + diffScore: number; + wtaScore: number; + rmpTags: string[]; + gradeDistributions: GradeDistribution[]; + ratingsDistribution: number[]; // temp +} + +interface GradeDistribution { + name: string; + series: ApexAxisChartSeries; +} + +const compareArrays = (a, b) => { + return JSON.stringify(a) === JSON.stringify(b); +}; + +export async function buildProfessorProfiles (payload: ShowCourseTabPayload) { + const { header, professors } = payload; + const nebulaCourse = await fetchNebulaCourse(header) + const nebulaProfessors = await Promise.all(professors.map(professor => { + const nameArray = professor.split(' ') + const firstName = nameArray[0] + const lastName = nameArray[nameArray.length - 1] + return fetchNebulaProfessor({ firstName: firstName, lastName: lastName}) + })) + const nebulaSections = await Promise.all(nebulaProfessors.map(professor => { + if (professor?._id === undefined) return null + return fetchNebulaSections({ courseReference: nebulaCourse._id, professorReference: professor._id }) + })) + const rmps = await requestProfessorsFromRmp({ professorNames: professors.map(prof => prof.split(' ')[0] + " " + prof.split(' ').at(-1)), schoolId: SCHOOL_ID }) + let professorProfiles: ProfessorProfileInterface[] = [] + for (let i = 0; i < professors.length; i++) { + const sectionsWithGrades = [] + for (let j = 0; j < nebulaSections[i]?.length; j++) { + const section = nebulaSections[i][j]; + if (section.grade_distribution.length !== 0 && !compareArrays(section.grade_distribution, Array(14).fill(0))) + sectionsWithGrades.push(section) + } + professorProfiles.push({ + name: professors[i], + profilePicUrl: nebulaProfessors[i]?.image_uri, + rmpId: rmps[i]?.legacyId, + rmpScore: rmps[i]?.avgRating ? (rmps[i]?.avgRating === 0 ? undefined : rmps[i]?.avgRating) : undefined, + diffScore: rmps[i]?.avgDifficulty ? (rmps[i].avgDifficulty === 0 ? undefined : rmps[i]?.avgDifficulty) : undefined, + wtaScore: rmps[i]?.wouldTakeAgainPercent ? (rmps[i]?.wouldTakeAgainPercent === -1 ? undefined : rmps[i]?.wouldTakeAgainPercent) : undefined, + rmpTags: rmps[i]?.teacherRatingTags.sort((a, b) => a.tagCount - b.tagCount).map(tag => tag.tagName), + gradeDistributions: sectionsWithGrades.length > 0 ? sectionsWithGrades.map(section => ({ name: [nebulaCourse.subject_prefix, nebulaCourse.course_number, section.section_number, section.academic_session.name].join(' '), series: [{name: "Students", data: section.grade_distribution}] })) : [{name: "No Data", series: [{name: "Students", data: []}]}], + ratingsDistribution: rmps[i] ? Object.values(rmps[i].ratingsDistribution).reverse().slice(1) : [] + }) + } + return professorProfiles +} \ No newline at end of file diff --git a/src/data/config.ts b/src/data/config.ts new file mode 100644 index 0000000..e38d06d --- /dev/null +++ b/src/data/config.ts @@ -0,0 +1,31 @@ +export const HEADERS = { + "Authorization": "Basic dGVzdDp0ZXN0", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", + "Content-Type": "application/json" +} + +export const PROFESSOR_QUERY = { + "query":"query RatingsListQuery($id: ID!) {node(id: $id) {... on Teacher {legacyId school {id} courseCodes {courseName courseCount} firstName lastName numRatings avgDifficulty avgRating department wouldTakeAgainPercent teacherRatingTags { tagCount tagName } ratingsDistribution { total r1 r2 r3 r4 r5 } }}}", + "variables": {} +} + +export const NEBULA_FETCH_OPTIONS = { + method: "GET", + headers: { + "x-api-key": unRegister("EM~eW}G<}4qx41fp{H=I]OZ5MF6T:1x{ | null { + try { + const res = await fetch(`https://api.utdnebula.com/course?course_number=${params.courseNumber}&subject_prefix=${params.subjectPrefix}`, NEBULA_FETCH_OPTIONS); + const json = await res.json(); + if (json.data == null) throw new Error("Null data"); + const data: CourseInterface = json.data[0]; + return data; + } catch (error) { + return null; + } } -export const PROFESSOR_QUERY = { - "query":"query RatingsListQuery($id: ID!) {node(id: $id) {... on Teacher {legacyId school {id} courseCodes {courseName courseCount} firstName lastName numRatings avgDifficulty avgRating department wouldTakeAgainPercent teacherRatingTags { tagCount tagName } ratingsDistribution { total r1 r2 r3 r4 r5 } }}}", - "variables": {} +export async function fetchNebulaProfessor(params: FetchProfessorParameters): Promise | null { + try { + const res = await fetch(`https://api.utdnebula.com/professor?first_name=${params.firstName}&last_name=${params.lastName}`, NEBULA_FETCH_OPTIONS); + const json = await res.json(); + if (json.data == null) throw new Error("Null data"); + const data: ProfessorInterface = json.data[0]; + return data; + } catch (error) { + return null; + } } -import type { fetchCourse_params, courseData, fetchProfessor_params, professorData } from "./nebulaInterfaces"; - -// Hash function to get the API key -function unRegister(myVar: string) -{ - let newVar = ""; - for (var i = 0; i < myVar.length; i++) - { - let a = myVar.charCodeAt(i); - a = ((a*2)-8)/2; - newVar = newVar.concat(String.fromCharCode(a)); - } - return newVar; -} - - -// TESTING INFO -// Install ts-node locally -// add "type": "module" to package.json - -// ts-node-esm fetch.ts - - - - -let NEBULA_API_KEY = "EM~eW}G<}4qx41fp{H=I]OZ5MF6T:1x{ | null { - try { - const res = await fetch(`https://api.utdnebula.com/course?course_number=${paramsObj.courseNumber}&subject_prefix=${paramsObj.subjectPrefix}`, fetchOptions); - const json = await res.json(); - if (json.data == null) throw new Error("Null data"); - const data: courseData = json.data[0]; - return data; - } catch (error) { - return null; - } -} - -async function fetchNebulaProfessor(paramsObj: fetchProfessor_params): Promise | null { - try { - const res = await fetch(`https://api.utdnebula.com/professor?first_name=${paramsObj.firstName}&last_name=${paramsObj.lastName}`, fetchOptions); - const json = await res.json(); - if (json.data == null) throw new Error("Null data"); - const data: professorData = json.data[0]; - return data; - } catch (error) { - return null; - } +export async function fetchNebulaSections(params: FetchSectionParameters): Promise | null { + try { + const res = await fetch(`https://api.utdnebula.com/section?course_reference=${params.courseReference}&professors=${params.professorReference}`, NEBULA_FETCH_OPTIONS); + const json = await res.json(); + if(json.data == null) throw new Error("Null data"); + const data: SectionInterface[] = json.data; + return data; + } catch(error) { + return null; + } } // Test function. Commented out. Uncomment to test. -console.log(await fetchNebulaCourse({courseNumber: "4337", subjectPrefix: "CS"})); -console.log(await fetchNebulaProfessor({firstName: "Scott", lastName: "Dollinger"})); \ No newline at end of file +// console.log(await fetchNebulaCourse({courseNumber: "4337", subjectPrefix: "CS"})); +// console.log(await fetchNebulaProfessor({firstName: "Scott", lastName: "Dollinger"})); +// console.log(await fetchNebulaSections({ courseReference: "623fedfabf28b6d88d6c7742", professorReference: "623fc346b8bc16815e8679a9" })) \ No newline at end of file diff --git a/src/data/fetchFromRmp.ts b/src/data/fetchFromRmp.ts index 127e1fb..ffa848b 100644 --- a/src/data/fetchFromRmp.ts +++ b/src/data/fetchFromRmp.ts @@ -1,5 +1,4 @@ -import fetch from "node-fetch" -import {HEADERS, PROFESSOR_QUERY} from "~data/fetch"; +import { HEADERS, PROFESSOR_QUERY } from "~data/config"; function getProfessorUrl(professorName: string, schoolId: string): string { return `https://www.ratemyprofessors.com/search/teachers?query=${encodeURIComponent(professorName)}&sid=${btoa(`School-${schoolId}`)}` @@ -14,13 +13,18 @@ function getProfessorUrls(professorNames: string[], schoolId: string): string[] function getProfessorIds(texts: string[], professorNames: string[]): string[] { const professorIds = [] + const lowerCaseProfessorNames = professorNames.map(name => name.toLowerCase()) texts.forEach(text => { - const regex = /"legacyId":(\d+).*?"firstName":"(\w+)","lastName":"(\w+)"/g; + let matched = false; + const regex = /"legacyId":(\d+).*?"firstName":"(.*?)","lastName":"(.*?)"/g; for (const match of text.matchAll(regex)) { - if (professorNames.includes(match[2] + " " + match[3])) { + console.log(match[2].split(' ')[0].toLowerCase() + " " + match[3].toLowerCase()) + if (lowerCaseProfessorNames.includes(match[2].split(' ')[0].toLowerCase() + " " + match[3].toLowerCase())) { professorIds.push(match[1]); + matched = true; } } + if (!matched) professorIds.push(null) }) return professorIds } @@ -53,6 +57,7 @@ function fetchWithGraphQl(graphQlUrlProps: any[], resolve) { ratings[i] = ratings[i]["data"]["node"]; } } + console.log(ratings) resolve(ratings) }) } @@ -61,10 +66,8 @@ export interface RmpRequest { professorNames: string[], schoolId: string } -export function requestProfessorsFromRmp(request: RmpRequest) { +export function requestProfessorsFromRmp(request: RmpRequest): Promise { return new Promise((resolve, reject) => { - console.log("Running request professors...") - const startTime = Date.now(); // make a list of urls for promises const professorUrls = getProfessorUrls(request.professorNames, request.schoolId) @@ -82,10 +85,37 @@ export function requestProfessorsFromRmp(request: RmpRequest) { fetchWithGraphQl(graphQlUrlProps, resolve) } ).catch(error => { - console.log(error) reject(error); }); - - console.log(Date.now() - startTime); }) +} + +interface RMPInterface { + avgDifficulty: number; + avgRating: number; + courseCodes: { + courseCount: number; + courseName: string; + }[]; + department: string; + firstName: string; + lastName: string; + legacyId: number; + numRatings: number; + ratingsDistribution: { + r1: number; + r2: number; + r3: number; + r4: number; + r5: number; + total: number; + }; + school: { + id: string; + }; + teacherRatingTags: { + tagCount: number; + tagName: string; + }[]; + wouldTakeAgainPercent: number; } \ No newline at end of file diff --git a/src/data/fetchSection.ts b/src/data/fetchSection.ts deleted file mode 100644 index 8271f71..0000000 --- a/src/data/fetchSection.ts +++ /dev/null @@ -1,67 +0,0 @@ - -type FetchSection_params = { - courseReference: string; - professorReference: string; -}; - -type Section = { - term: string; - title: string; - course_number: string; - school: string; - location: string; - activity_type: string; - class_number: string; - days: string; - assistants: string; - times: string; - topic: string; - core_area: string; - department: string; - section_name: string; - course_prefix: string; - instructors: string; - section_number: string; -}; - -type SectionList = { - sections: Section[]; -}; - -//Hash to get API key -function unRegister(myVar: string) { - let newVar = ""; - for(var i = 0; i < myVar.length; i++) { - let a = myVar.charCodeAt(i); - a = ((a*2)-8)/2; - newVar = newVar.concat(String.fromCharCode(a)); - } - return newVar; -} - -//Nebula key -let NEBULA_API_KEY = "EM~eW}G<}4qx41fp{H=I]OZ5MF6T:1x{ { - try { - const res = await fetch(`https://api.utdnebula.com/section?course_reference=${paramsObj.courseReference}&professors=${paramsObj.professorReference}`, fetchOptions); - const json = await res.json(); - if(json.data == null) { - throw new Error("Null data"); - } - const data: SectionList = json.data; - return data; - } catch(error) { - return null; - } -} - -console.log((await fetchNebulaSections({courseReference: "3377", professorReference: "Peterson"}))); diff --git a/src/data/interfaces.ts b/src/data/interfaces.ts new file mode 100644 index 0000000..877eb9e --- /dev/null +++ b/src/data/interfaces.ts @@ -0,0 +1,88 @@ +export interface FetchProfessorParameters { + firstName: string; + lastName: string; +}; + +export interface FetchCourseParameters { + subjectPrefix: string; + courseNumber: string; +} + +export interface FetchSectionParameters { + courseReference: string; + professorReference: string; +}; + +interface Requisites { + options: any[]; + required: number; + type: string; +} + +export interface CourseInterface { + __v: number; + _id: string; + activity_type: string; + class_level: string; + co_or_pre_requisites: Requisites; + corequisites: Requisites; + course_number: string; + credit_hours: string; + description: string; + grading: string; + internal_course_number: string; + laboratory_contact_hours: string; + lecture_contact_hours: string; + offering_frequency: string; + prerequisites: Requisites; + school: string; + sections: string[]; + subject_prefix: string; + title: string; +} + +interface Office { + building: string; + room: string; + map_uri: string +} + +export interface ProfessorInterface { + __v: number; + _id: string; + email: string; + first_name: string; + image_uri: string; + last_name: string; + office: Office; + office_hours: any[]; + phone_number: string; + profile_uri: string; + sections: string[]; + titles: string[]; +} + +export interface SectionInterface { + __v: number; + _id: string; + academic_session: { + end_date: string; + name: string; + start_date: string; + }; + attributes: any[]; + core_flags: any[]; + course_reference: string; + grade_distribution: number[]; + instruction_mode: string; + internal_class_number: string; + meetings: any[]; + professors: string[]; + section_corequisites: { + options: any[]; + type: string; + }; + section_number: string; + syllabus_uri: string; + teaching_assistants: any[]; +}; \ No newline at end of file diff --git a/src/data/nebulaInterfaces.ts b/src/data/nebulaInterfaces.ts deleted file mode 100644 index 160ed3c..0000000 --- a/src/data/nebulaInterfaces.ts +++ /dev/null @@ -1,58 +0,0 @@ -export interface fetchProfessor_params { - firstName: string, - lastName: string -}; - -export interface fetchCourse_params { - subjectPrefix: string, - courseNumber: string -} - -export interface requisites { - options: Array, - required: number, - type: string -} - -export interface courseData { - __v: number, - _id: string, - activity_type: string, - class_level: string, - co_or_pre_requisites: requisites, - corequisites: requisites, - course_number: string, - credit_hours: string, - description: string, - grading: string, - internal_course_number: string, - laboratory_contact_hours: string, - lecture_contact_hours: string, - offering_frequency: string, - prerequisites: requisites, - school: string, - sections: Array, - subject_prefix: string, - title: string, -} - -export interface office { - building: string, - room: string, - map_uri: string -} - -export interface professorData { - __v: number, - _id: string, - email: string, - first_name: string, - image_uri: string, - last_name: string, - office: office, - office_hours: Array, - phone_number: string, - profile_uri: string, - sections: Array, - titles: Array, -} diff --git a/src/popup.tsx b/src/popup.tsx index fe3fd94..e9813de 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -1,18 +1,7 @@ import { MemoryRouter } from "react-router-dom" import { Routing } from "~/routes" -import { sendToBackground } from "@plasmohq/messaging" import "~/style.css" -// Example of how to fetch the scraped data from the background script, given that it exists -let funct = async () => { -const resp = await sendToBackground({ - name: "getScrapeData", -}) -console.log("Response is ",resp) -} - -funct(); - function IndexPopup() { return ( diff --git a/src/routes/CoursePage.tsx b/src/routes/CoursePage.tsx index db53296..47b0eb4 100644 --- a/src/routes/CoursePage.tsx +++ b/src/routes/CoursePage.tsx @@ -1,13 +1,48 @@ +import { sendToBackground } from "@plasmohq/messaging"; +import { useEffect, useState } from "react"; +import { Rings } from "react-loader-spinner"; import { useLocation } from "react-router-dom"; +import type { ShowCourseTabPayload } from "~background"; +import { Landing } from "~components/Landing"; +import { Loading } from "~components/Loading"; import { MiniProfessor } from "~components/MiniProfessor" -import type { ProfessorProfileInterface } from "./about"; +import type { GradeDistribution } from "~components/ProfileGrades"; +import { buildProfessorProfiles, ProfessorProfileInterface } from "~data/builder"; + +// Example of how to fetch the scraped data from the background script, given that it exists +async function getCourseData () { + const response: ShowCourseTabPayload = await sendToBackground({ + name: "getScrapeData", + }) + return response; +} export const CoursePage = () => { // TODO: CHANGE INTERFACE const { state } : { state: ProfessorProfileInterface[] } = useLocation(); + const [ loading, setLoading ] = useState(true) + const [ profiles, setProfiles ] = useState(null) + + useEffect(() => { + if (!state) { + setLoading(true) + getCourseData().then(payload => { + buildProfessorProfiles(payload).then(profiles => { + setProfiles(profiles) + }).finally(() => setLoading(false)) + }) + } else { + setProfiles(state) + setLoading(false) + } + }, []) return(
- {state.map((item, index) =>
)} + { !loading && profiles && + profiles.map((item, index) =>
) + } + { loading && } + { !loading && !profiles && }
) } \ No newline at end of file diff --git a/src/routes/ProfessorProfile.tsx b/src/routes/ProfessorProfile.tsx index 8694279..a0c230e 100644 --- a/src/routes/ProfessorProfile.tsx +++ b/src/routes/ProfessorProfile.tsx @@ -6,23 +6,31 @@ import { ProfileGrades } from "~components/ProfileGrades" import { ProfileHeader } from "~components/ProfileHeader" import { RmpRatings } from "~components/RmpRatings" import { RmpTag } from "~components/RmpTag" -import type { ProfessorProfileInterface } from "./about" +import type { ProfessorProfileInterface } from "~data/builder" export const ProfessorProfile = () => { - const { state }: { state: ProfessorProfileInterface } = useLocation(); + const { state } : { state: { professorData: ProfessorProfileInterface, profiles: ProfessorProfileInterface[] } } = useLocation(); + const { professorData, profiles } = state; + + const compareArrays = (a, b) => { + return JSON.stringify(a) === JSON.stringify(b); + }; + return (
- +
{/* spacer */} - +
{/* RMP Tag area */} - {state.rmpTags.sort((a, b) => b.length - a.length).map((item, index) => - + {professorData.rmpTags?.slice(0, 4).map((item: string, index: number) => + )}
- - + { professorData.ratingsDistribution.length > 0 && !compareArrays(professorData.ratingsDistribution, Array(5).fill(0)) && + + } +
diff --git a/src/routes/about.tsx b/src/routes/about.tsx deleted file mode 100644 index ac42581..0000000 --- a/src/routes/about.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { NavigateFunction, useNavigate } from "react-router-dom" -import type { GradeDistribution } from "~components/ProfileGrades"; - -export interface ProfessorProfileInterface { - name: string; - profilePicUrl: string; - rmpScore: number; - diffScore: number; - wtaScore: number; - rmpTags: string[]; - gradeDistributions: GradeDistribution[]; - ratingsDistribution: number[]; // temp -} - -/** Temporary for testing */ -export const mockData: ProfessorProfileInterface = { - name: "Johny Deere", - profilePicUrl: "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcSUe2eaB9QYVkoJORkwnG2yfpPRqpqvRyUkWXOfvLOirm1mudvx", - rmpScore: 4.3, - diffScore: 2.55, - wtaScore: 100, - rmpTags: ["GREAT LECTURES", "SKIP CLASS? YOU WON'T PASS", "FRIENDLY", "TOUGH GRADER", "LOTS OF HOMEWORK"], - gradeDistributions: [ - { - name: "MATH 2418.003 (18S)", - series: [{ - name: 'Students', - data: [30, 40, 35, 50, 49, 60, 70, 79, 80, 10, 24, 65, 12, 50] - }] - }, - { - name: "MATH 2418.003 (17S)", - series: [{ - name: 'Students', - data: [10, 40, 35, 50, 29, 65, 70, 79, 30, 15, 24, 35, 12, 20] - }] - } - ], - ratingsDistribution: [20, 13, 20, 33, 7] -} - -export const About = () => { - const navigation: NavigateFunction = useNavigate() - - const onNextPage = (): void => { - navigation("/about", { state: mockData }) - } - - return ( -
- About page - -
- ) -} \ No newline at end of file diff --git a/src/routes/home.tsx b/src/routes/home.tsx deleted file mode 100644 index 0f77396..0000000 --- a/src/routes/home.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { NavigateFunction, useNavigate } from "react-router-dom" -import { mockData } from "./about" - -export const Home = () => { - const navigation: NavigateFunction = useNavigate() - - const onNextPage = (): void => { - navigation("/test", { state: [mockData, mockData, mockData] }) - } - - return ( -
- Home page - -
- ) -} \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index db630dd..7959470 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,14 +1,11 @@ import { Route, Routes } from "react-router-dom" -import { About } from "./about" import { CoursePage } from "./CoursePage" -import { Home } from "./home" import { ProfessorProfile } from "./ProfessorProfile" export const Routing = () => ( - } /> - } /> - } /> + } /> + } /> ) \ No newline at end of file diff --git a/src/utils/styling.ts b/src/utils/styling.ts index 6415a2a..5fac1d4 100644 --- a/src/utils/styling.ts +++ b/src/utils/styling.ts @@ -14,6 +14,11 @@ export const gradeChartOptions: ApexOptions = { distributed: true } }, + noData: { + text: "No grade data found", + align: "center", + verticalAlign: "middle", + }, dataLabels: { enabled: false }, @@ -72,6 +77,11 @@ export const ratingsChartOptions: ApexOptions = { }, id: 'ratings-distribution' }, + noData: { + text: "No data found", + align: "center", + verticalAlign: "middle", + }, grid: { padding: { bottom: -95 @@ -114,6 +124,11 @@ export const miniGradeChartOptions: ApexOptions = { color: '#9B9B9B' }, }, + noData: { + text: "No grade data found", + align: "center", + verticalAlign: "middle", + }, dataLabels: { enabled: false },