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
+
+
+
+ )
+}
\ 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
- Home
-
- )
-}
\ 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
- About
-
- )
-}
\ 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
},