Compare commits
18 Commits
v2.2.1
...
fix/vite-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d291606ead | ||
|
|
46fe591fa7 | ||
|
|
ea34a7807f | ||
|
|
8f7e1bc0af | ||
|
|
9fc1098ef7 | ||
|
|
ae094416fc | ||
|
|
2e7dac1e3e | ||
|
|
7bea23a655 | ||
|
|
3d28869e92 | ||
|
|
f0f1f0b365 | ||
| be861b823c | |||
|
|
95de8df372 | ||
| 5994ded8be | |||
|
|
7b401add15 | ||
|
|
2d92dd47f0 | ||
|
|
eb8141ee8c | ||
|
|
2a50f5580d | ||
|
|
65bfb1d129 |
@@ -7,3 +7,6 @@ insert_final_newline = true
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
[*.nix]
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
SENTRY_ORG=longhorn-developers
|
||||||
|
SENTRY_PROJECT=ut-registration-plus
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -211,3 +211,5 @@ sketch
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
storybook-static/
|
storybook-static/
|
||||||
package/
|
package/
|
||||||
|
|
||||||
|
.direnv/
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -26,8 +26,9 @@
|
|||||||
## Toolchain
|
## Toolchain
|
||||||
|
|
||||||
- React v20.9.0 (LTS)
|
- React v20.9.0 (LTS)
|
||||||
- TypeScript
|
- TypeScript v5.x
|
||||||
- Vite 5
|
- Vite v5.x
|
||||||
|
- pnpm v10.x
|
||||||
- UnoCSS
|
- UnoCSS
|
||||||
- ESLint
|
- ESLint
|
||||||
- Prettier
|
- Prettier
|
||||||
@@ -184,8 +185,9 @@ We maintain a strict code of conduct. By contributing, you agree to adhere to th
|
|||||||
Special thanks to the developers and contributors behind these amazing tools and libraries:
|
Special thanks to the developers and contributors behind these amazing tools and libraries:
|
||||||
|
|
||||||
- React v20.9.0 (LTS)
|
- React v20.9.0 (LTS)
|
||||||
- TypeScript
|
- TypeScript v5.x
|
||||||
- Vite 5
|
- Vite v5.x
|
||||||
|
- pnpm v10.x
|
||||||
- UnoCSS
|
- UnoCSS
|
||||||
- ESLint
|
- ESLint
|
||||||
- Prettier
|
- Prettier
|
||||||
|
|||||||
0
docs/WebSocket-Implementation-Tutorial.md
Normal file
0
docs/WebSocket-Implementation-Tutorial.md
Normal file
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1754725699,
|
||||||
|
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
36
flake.nix
Normal file
36
flake.nix
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = (import nixpkgs { inherit system; });
|
||||||
|
in
|
||||||
|
{
|
||||||
|
formatter = pkgs.nixfmt-rfc-style;
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "utrp-dev";
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
nodejs_20 # v20.19.4
|
||||||
|
pnpm_10 # v10.14.0
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "UTRP Nix Flake Environment Loaded"
|
||||||
|
echo "Node: $(node --version)"
|
||||||
|
echo "pnpm: $(pnpm --version)"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
46
package.json
46
package.json
@@ -39,8 +39,8 @@
|
|||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@sentry/react": "^8.55.0",
|
"@sentry/react": "^8.55.0",
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"@unocss/vite": "^0.63.6",
|
"@unocss/vite": "^66.5.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"chrome-extension-toolkit": "^0.0.54",
|
"chrome-extension-toolkit": "^0.0.54",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"conventional-changelog": "^6.0.0",
|
"conventional-changelog": "^6.0.0",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"@commitlint/cli": "^19.7.1",
|
"@commitlint/cli": "^19.7.1",
|
||||||
"@commitlint/config-conventional": "^19.7.1",
|
"@commitlint/config-conventional": "^19.7.1",
|
||||||
"@commitlint/types": "^19.5.0",
|
"@commitlint/types": "^19.5.0",
|
||||||
"@crxjs/vite-plugin": "2.0.0-beta.21",
|
"@crxjs/vite-plugin": "2.2.0",
|
||||||
"@iconify-json/bi": "^1.2.2",
|
"@iconify-json/bi": "^1.2.2",
|
||||||
"@iconify-json/ic": "^1.2.2",
|
"@iconify-json/ic": "^1.2.2",
|
||||||
"@iconify-json/iconoir": "^1.2.7",
|
"@iconify-json/iconoir": "^1.2.7",
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"@types/conventional-changelog": "^3.1.5",
|
"@types/conventional-changelog": "^3.1.5",
|
||||||
"@types/gulp": "^4.0.17",
|
"@types/gulp": "^4.0.17",
|
||||||
"@types/gulp-zip": "^4.0.4",
|
"@types/gulp-zip": "^4.0.4",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.18.0",
|
||||||
"@types/prompts": "^2.4.9",
|
"@types/prompts": "^2.4.9",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
@@ -98,14 +98,14 @@
|
|||||||
"@types/sql.js": "^1.4.9",
|
"@types/sql.js": "^1.4.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"@unocss/eslint-config": "^0.63.6",
|
"@unocss/eslint-config": "^66.5.1",
|
||||||
"@unocss/postcss": "^0.63.6",
|
"@unocss/postcss": "^66.5.1",
|
||||||
"@unocss/preset-uno": "^0.63.6",
|
"@unocss/preset-web-fonts": "^66.5.1",
|
||||||
"@unocss/preset-web-fonts": "^0.63.6",
|
"@unocss/preset-wind4": "^66.5.1",
|
||||||
"@unocss/reset": "^0.63.6",
|
"@unocss/reset": "^66.5.1",
|
||||||
"@unocss/transformer-directives": "^0.63.6",
|
"@unocss/transformer-directives": "^66.5.1",
|
||||||
"@unocss/transformer-variant-group": "^0.63.6",
|
"@unocss/transformer-variant-group": "^66.5.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
"@vitejs/plugin-react-swc": "^4.0.1",
|
||||||
"@vitest/coverage-v8": "^2.1.9",
|
"@vitest/coverage-v8": "^2.1.9",
|
||||||
"@vitest/ui": "^2.1.9",
|
"@vitest/ui": "^2.1.9",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
@@ -141,11 +141,11 @@
|
|||||||
"semantic-release": "^24.2.3",
|
"semantic-release": "^24.2.3",
|
||||||
"storybook": "^8.6.0",
|
"storybook": "^8.6.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"unocss": "^0.63.6",
|
"unocss": "^66.5.1",
|
||||||
"unocss-preset-primitives": "0.0.2-beta.1",
|
"unocss-preset-primitives": "0.0.2-beta.2",
|
||||||
"unplugin-icons": "^0.19.3",
|
"unplugin-icons": "^0.19.3",
|
||||||
"vite": "^5.4.14",
|
"vite": "^7.1.5",
|
||||||
"vite-plugin-inspect": "^0.8.9",
|
"vite-plugin-inspect": "^11.3.3",
|
||||||
"vitest": "^2.1.9"
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
"engineStrict": true,
|
"engineStrict": true,
|
||||||
@@ -154,15 +154,15 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch",
|
"@unocss/vite": "patches/@unocss__vite.patch",
|
||||||
"@unocss/vite": "patches/@unocss__vite.patch"
|
"@crxjs/vite-plugin": "patches/@crxjs__vite-plugin.patch"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"onlyBuiltDependencies": [
|
||||||
"es-module-lexer": "^1.5.4"
|
"@swc/core"
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.9.0",
|
"node": "20.19.4",
|
||||||
"pnpm": "10.6.5"
|
"pnpm": "10.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1615
patches/@crxjs__vite-plugin.patch
Normal file
1615
patches/@crxjs__vite-plugin.patch
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
|||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
|
||||||
index 5c3f6291168987c56b816428080e6f1fe9de7107..abaf6290fe9454ae036a81eacbe7dc3be2fdfbc3 100644
|
|
||||||
--- a/dist/index.mjs
|
|
||||||
+++ b/dist/index.mjs
|
|
||||||
@@ -499,16 +499,43 @@ ${sourceMap}
|
|
||||||
}),
|
|
||||||
mergeMap(async ({ target, code, deps }) => {
|
|
||||||
await lexer.init;
|
|
||||||
- const [imports] = lexer.parse(code, fileName);
|
|
||||||
+ const [imports, exports] = lexer.parse(code, fileName);
|
|
||||||
const depSet = new Set(deps);
|
|
||||||
const magic = new MagicString(code);
|
|
||||||
- for (const i of imports)
|
|
||||||
+ for (const i of imports) {
|
|
||||||
if (i.n) {
|
|
||||||
depSet.add(i.n);
|
|
||||||
const fileName2 = getFileName({ type: "module", id: i.n });
|
|
||||||
const fullImport = code.substring(i.s, i.e);
|
|
||||||
- magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
|
|
||||||
+ const hmrTimestamp = fullImport.match(/\bt=\d{13}&?\b/);
|
|
||||||
+ magic.overwrite(
|
|
||||||
+ i.s,
|
|
||||||
+ i.e,
|
|
||||||
+ fullImport.replace(
|
|
||||||
+ i.n,
|
|
||||||
+ `/${fileName2}${hmrTimestamp ? `?${hmrTimestamp[0]}` : ""}`
|
|
||||||
+ )
|
|
||||||
+ );
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ for (const e of exports) {
|
|
||||||
+ if (e.n === "default") {
|
|
||||||
+ const regex = /\s+['"](.*)['"]/y;
|
|
||||||
+ regex.lastIndex = e.e;
|
|
||||||
+ const fullExport = regex.exec(code)?.[1];
|
|
||||||
+ if (!fullExport)
|
|
||||||
+ continue;
|
|
||||||
+ const start = regex.lastIndex - fullExport.length - 1;
|
|
||||||
+ const end = regex.lastIndex - 1;
|
|
||||||
+ if (fullExport.startsWith("/node_modules")) {
|
|
||||||
+ magic.overwrite(
|
|
||||||
+ start,
|
|
||||||
+ end,
|
|
||||||
+ `http://localhost:5173${fullExport}`
|
|
||||||
+ );
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
+ }
|
|
||||||
return { target, source: magic.toString(), deps: [...depSet] };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -1229,10 +1256,14 @@ const pluginHMR = () => {
|
|
||||||
handleHotUpdate({ modules, server }) {
|
|
||||||
const { root } = server.config;
|
|
||||||
const relFiles = /* @__PURE__ */ new Set();
|
|
||||||
- for (const m of modules)
|
|
||||||
+ function getRelFile(file) {
|
|
||||||
+ return file.startsWith(root) ? file.slice(server.config.root.length) : file;
|
|
||||||
+ }
|
|
||||||
+ for (const m of modules) {
|
|
||||||
if (m.id?.startsWith(root)) {
|
|
||||||
relFiles.add(m.id.slice(server.config.root.length));
|
|
||||||
}
|
|
||||||
+ }
|
|
||||||
if (inputManifestFiles.background.length) {
|
|
||||||
const background = prefix$1("/", inputManifestFiles.background[0]);
|
|
||||||
if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
|
|
||||||
@@ -1244,7 +1275,14 @@ const pluginHMR = () => {
|
|
||||||
for (const [key, script] of contentScripts)
|
|
||||||
if (key === script.id) {
|
|
||||||
if (relFiles.has(script.id) || modules.some(isImporter(join(server.config.root, script.id)))) {
|
|
||||||
- relFiles.forEach((relFile) => update(relFile));
|
|
||||||
+ modules.filter((mod) => mod.id?.startsWith(root)).forEach((mod) => {
|
|
||||||
+ update(getRelFile(mod.id));
|
|
||||||
+ if (mod.file?.endsWith(".scss")) {
|
|
||||||
+ mod.importers.forEach((imp) => {
|
|
||||||
+ update(getRelFile(imp.id));
|
|
||||||
+ });
|
|
||||||
+ }
|
|
||||||
+ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1882,7 +1920,7 @@ const pluginWebAccessibleResources = () => {
|
|
||||||
if (contentScripts.size > 0) {
|
|
||||||
const viteManifest = parseJsonAsset(
|
|
||||||
bundle,
|
|
||||||
- "manifest.json"
|
|
||||||
+ ".vite/manifest.json"
|
|
||||||
);
|
|
||||||
const viteFiles = /* @__PURE__ */ new Map();
|
|
||||||
for (const [, file] of Object.entries(viteManifest))
|
|
||||||
diff --git a/package.json b/package.json
|
|
||||||
index e0c47ae66ff399ad3a78abf38d8d93d1f038c55d..f84eb09ffbb5c41094935dd06e04ffe831e2d05a 100644
|
|
||||||
--- a/package.json
|
|
||||||
+++ b/package.json
|
|
||||||
@@ -70,7 +70,7 @@
|
|
||||||
"connect-injector": "^0.4.4",
|
|
||||||
"convert-source-map": "^1.7.0",
|
|
||||||
"debug": "^4.3.3",
|
|
||||||
- "es-module-lexer": "^0.10.0",
|
|
||||||
+ "es-module-lexer": "^1.4.1",
|
|
||||||
"fast-glob": "^3.2.11",
|
|
||||||
"fs-extra": "^10.0.1",
|
|
||||||
"jsesc": "^3.0.2",
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
index 7210f5fd650a0b7bb36b467fff85feb0d8e4ec63..c8f98bd314daec0b91c514ea9d9fc2b79cea8502 100644
|
index 3b94c870f00ce7e70be208199db48a3bd5569830..4aaaec7d5b656f1c4066c6295d5228d746420d17 100644
|
||||||
--- a/dist/index.mjs
|
--- a/dist/index.mjs
|
||||||
+++ b/dist/index.mjs
|
+++ b/dist/index.mjs
|
||||||
@@ -369,15 +369,15 @@ const VIRTUAL_ENTRY_ALIAS = [
|
@@ -24,8 +24,8 @@ const VIRTUAL_ENTRY_ALIAS = [
|
||||||
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
|
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
|
||||||
];
|
];
|
||||||
const LAYER_MARK_ALL = "__ALL__";
|
const LAYER_MARK_ALL = "__ALL__";
|
||||||
@@ -10,27 +10,19 @@ index 7210f5fd650a0b7bb36b467fff85feb0d8e4ec63..c8f98bd314daec0b91c514ea9d9fc2b7
|
|||||||
-const RESOLVED_ID_RE = /[/\\]__uno(?:_(.*?))?\.css$/;
|
-const RESOLVED_ID_RE = /[/\\]__uno(?:_(.*?))?\.css$/;
|
||||||
+const RESOLVED_ID_WITH_QUERY_RE = /[/\\]uno(_.*?)?\.css(\?.*)?$/;
|
+const RESOLVED_ID_WITH_QUERY_RE = /[/\\]uno(_.*?)?\.css(\?.*)?$/;
|
||||||
+const RESOLVED_ID_RE = /[/\\]uno(?:_(.*?))?\.css$/;
|
+const RESOLVED_ID_RE = /[/\\]uno(?:_(.*?))?\.css$/;
|
||||||
function resolveId(id) {
|
|
||||||
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
|
const defaultPipelineExclude = [cssIdRE];
|
||||||
return id;
|
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|vine.ts|mdx?|astro|elm|php|phtml|html)($|\?)/];
|
||||||
|
@@ -468,7 +468,7 @@ function resolveId(id, importer) {
|
||||||
for (const alias of VIRTUAL_ENTRY_ALIAS) {
|
for (const alias of VIRTUAL_ENTRY_ALIAS) {
|
||||||
const match = id.match(alias);
|
const match = id.match(alias);
|
||||||
if (match) {
|
if (match) {
|
||||||
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
|
- let virtual = match[1] ? `__uno_${match[1]}.css` : "__uno.css";
|
||||||
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
|
+ let virtual = match[1] ? `uno_${match[1]}.css` : "uno.css";
|
||||||
}
|
virtual += match[2] || "";
|
||||||
}
|
if (importer)
|
||||||
}
|
virtual = resolve$1(importer, "..", virtual);
|
||||||
@@ -652,7 +652,7 @@ function GlobalModeBuildPlugin(ctx) {
|
@@ -813,7 +813,7 @@ function GlobalModeDevPlugin(ctx) {
|
||||||
css = await applyCssTransform(css, fakeCssId, options.dir, this);
|
|
||||||
const transformHandler = "handler" in cssPost.transform ? cssPost.transform.handler : cssPost.transform;
|
|
||||||
if (isLegacy) {
|
|
||||||
- await transformHandler.call({}, css, "/__uno.css");
|
|
||||||
+ await transformHandler.call({}, css, "/uno.css");
|
|
||||||
} else {
|
|
||||||
const hash = getHash(css);
|
|
||||||
await transformHandler.call({}, getHashPlaceholder(hash), fakeCssId);
|
|
||||||
@@ -914,7 +914,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
|
|
||||||
const { hash, css } = await generateCSS(layer);
|
const { hash, css } = await generateCSS(layer);
|
||||||
return {
|
return {
|
||||||
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
|
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
|
||||||
@@ -39,7 +31,7 @@ index 7210f5fd650a0b7bb36b467fff85feb0d8e4ec63..c8f98bd314daec0b91c514ea9d9fc2b7
|
|||||||
map: { mappings: "" }
|
map: { mappings: "" }
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -933,7 +933,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
|
@@ -832,7 +832,7 @@ function GlobalModeDevPlugin(ctx) {
|
||||||
if (layer && code.includes("import.meta.hot")) {
|
if (layer && code.includes("import.meta.hot")) {
|
||||||
let hmr = `
|
let hmr = `
|
||||||
try {
|
try {
|
||||||
|
|||||||
2634
pnpm-lock.yaml
generated
2634
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -38,7 +38,12 @@ const manifest = defineManifest(async () => ({
|
|||||||
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
|
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
|
||||||
action: {
|
action: {
|
||||||
default_popup: 'src/pages/popup/index.html',
|
default_popup: 'src/pages/popup/index.html',
|
||||||
default_icon: `icons/icon_${mode}_32.png`,
|
default_icon: {
|
||||||
|
'16': `icons/icon_${mode}_16.png`,
|
||||||
|
'32': `icons/icon_${mode}_32.png`,
|
||||||
|
'48': `icons/icon_${mode}_48.png`,
|
||||||
|
'128': `icons/icon_${mode}_128.png`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
'16': `icons/icon_${mode}_16.png`,
|
'16': `icons/icon_${mode}_16.png`,
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export default async function createSchedule(scheduleName: string) {
|
|||||||
|
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
|
||||||
|
// Automatically switch to the new schedule
|
||||||
|
await UserScheduleStore.set('activeIndex', schedules.length - 1);
|
||||||
|
|
||||||
// If there is only one schedule, set the active index to the new schedule
|
// If there is only one schedule, set the active index to the new schedule
|
||||||
if (schedules.length <= 1) {
|
if (schedules.length <= 1) {
|
||||||
await UserScheduleStore.set('activeIndex', 0);
|
await UserScheduleStore.set('activeIndex', 0);
|
||||||
|
|||||||
@@ -31,5 +31,9 @@ export default async function duplicateSchedule(scheduleId: string): Promise<str
|
|||||||
} satisfies typeof schedule);
|
} satisfies typeof schedule);
|
||||||
|
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
|
||||||
|
// Automatically switch to the duplicated schedule
|
||||||
|
await UserScheduleStore.set('activeIndex', scheduleIndex + 1);
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import CourseCatalogMain from '@views/components/CourseCatalogMain';
|
import CourseCatalogMain from '@views/components/CourseCatalogMain';
|
||||||
import InjectedButton from '@views/components/injected/AddAllButton';
|
import InjectedButton from '@views/components/injected/AddAllButton';
|
||||||
import DaysCheckbox from '@views/components/injected/DaysCheckbox';
|
import DaysCheckbox from '@views/components/injected/DaysCheckbox';
|
||||||
|
import ShadedResults from '@views/components/injected/SearchResultShader';
|
||||||
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
|
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
@@ -9,9 +10,8 @@ const support = getSiteSupport(window.location.href);
|
|||||||
|
|
||||||
const renderComponent = (Component: React.ComponentType) => {
|
const renderComponent = (Component: React.ComponentType) => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.id = 'extension-root';
|
|
||||||
document.body.appendChild(container);
|
|
||||||
|
|
||||||
|
// all components are portaled away, not actually rendered to screen
|
||||||
createRoot(container).render(
|
createRoot(container).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Component />
|
<Component />
|
||||||
@@ -30,3 +30,7 @@ if (support === SiteSupport.MY_UT) {
|
|||||||
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
|
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
|
||||||
renderComponent(DaysCheckbox);
|
renderComponent(DaysCheckbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (support === SiteSupport.COURSE_CATALOG_KWS) {
|
||||||
|
renderComponent(ShadedResults);
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export type Semester = {
|
|||||||
export class Course {
|
export class Course {
|
||||||
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
|
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
|
||||||
uniqueId!: number;
|
uniqueId!: number;
|
||||||
/** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */
|
/**
|
||||||
|
* This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H.
|
||||||
|
* UT prefixes summer courses with f, s, n, or w:
|
||||||
|
* [f]irst term, [s]econd term, [n]ine week term, [w]hole term.
|
||||||
|
* So, the first term of PSY 301 over the summer would be 'f301'
|
||||||
|
*/
|
||||||
number!: string;
|
number!: string;
|
||||||
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
|
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
|
||||||
fullName!: string;
|
fullName!: string;
|
||||||
@@ -91,6 +96,46 @@ export class Course {
|
|||||||
}
|
}
|
||||||
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
|
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
|
||||||
this.core = course.core ?? [];
|
this.core = course.core ?? [];
|
||||||
|
if (course.semester.season === 'Summer') {
|
||||||
|
// A bug from and old version put the summer term in the course,
|
||||||
|
// so we need to handle that case
|
||||||
|
const { department, number } = Course.cleanSummerTerm(course.department, course.number);
|
||||||
|
this.department = department;
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due to a bug in an older version, the summer term was included in the course department code,
|
||||||
|
* instead of the course number.
|
||||||
|
* UT prefixes summer courses with f, s, n, or w:
|
||||||
|
* [f]irst term, [s]econd term, [n]ine week term, [w]hole term
|
||||||
|
*
|
||||||
|
* @param department - The course department code, like 'C S'
|
||||||
|
* @param number - The course number, like '314H'
|
||||||
|
* @returns The properly formatted department and course number
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* cleanSummerTerm('C S', '314H') // { department: 'C S', number: '314H' }
|
||||||
|
* cleanSummerTerm('P R', 'f378') // { department: 'P R', number: 'f378' }
|
||||||
|
* cleanSummerTerm('P R f', '378') // { department: 'P R', number: 'f378' }
|
||||||
|
* cleanSummerTerm('P S', 'n303') // { department: 'P S', number: 'n303' }
|
||||||
|
* cleanSummerTerm('P S n', '303') // { department: 'P S', number: 'n303' }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static cleanSummerTerm(department: string, number: string): { department: string; number: string } {
|
||||||
|
// UT prefixes summer courses with f, s, n, or w:
|
||||||
|
// [f]irst term, [s]econd term, [n]ine week term, [w]hole term
|
||||||
|
const summerTerm = department.match(/[fsnw]$/);
|
||||||
|
|
||||||
|
if (!summerTerm) {
|
||||||
|
return { department, number };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
department: department.slice(0, -1).trim(),
|
||||||
|
number: summerTerm[0] + number,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,6 +156,18 @@ export class Course {
|
|||||||
|
|
||||||
return conflicts;
|
return conflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The course number without the summer term
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const c = new Course({ number: 'f301', ... });
|
||||||
|
* c.getNumberWithoutTerm() // '301'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getNumberWithoutTerm(): string {
|
||||||
|
return this.number.replace(/^\D/, ''); // Remove nondigit at start, if it exists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
57
src/shared/types/tests/Course.test.ts
Normal file
57
src/shared/types/tests/Course.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Course } from '../Course';
|
||||||
|
|
||||||
|
describe('Course::cleanSummerTerm', () => {
|
||||||
|
it("shouldn't affect already cleaned summer terms", () => {
|
||||||
|
const inputs = [
|
||||||
|
['C S', '314H'],
|
||||||
|
['P R', 'f378'],
|
||||||
|
['P S', 'f303'],
|
||||||
|
['WGS', 's301'],
|
||||||
|
['S W', 'n360K'],
|
||||||
|
['GOV', 'w312L'],
|
||||||
|
['J', 's311F'],
|
||||||
|
['J S', '311F'],
|
||||||
|
] as const;
|
||||||
|
const expected = [
|
||||||
|
{ department: 'C S', number: '314H' },
|
||||||
|
{ department: 'P R', number: 'f378' },
|
||||||
|
{ department: 'P S', number: 'f303' },
|
||||||
|
{ department: 'WGS', number: 's301' },
|
||||||
|
{ department: 'S W', number: 'n360K' },
|
||||||
|
{ department: 'GOV', number: 'w312L' },
|
||||||
|
{ department: 'J', number: 's311F' },
|
||||||
|
{ department: 'J S', number: '311F' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1]));
|
||||||
|
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move summer term indicator to course number', () => {
|
||||||
|
const inputs = [
|
||||||
|
['P R f', '378'],
|
||||||
|
['P S f', '303'],
|
||||||
|
['WGS s', '301'],
|
||||||
|
['S W n', '360K'],
|
||||||
|
['GOV w', '312L'],
|
||||||
|
['J s', '311F'],
|
||||||
|
['J S', '311F'],
|
||||||
|
] as const;
|
||||||
|
const expected = [
|
||||||
|
{ department: 'P R', number: 'f378' },
|
||||||
|
{ department: 'P S', number: 'f303' },
|
||||||
|
{ department: 'WGS', number: 's301' },
|
||||||
|
{ department: 'S W', number: 'n360K' },
|
||||||
|
{ department: 'GOV', number: 'w312L' },
|
||||||
|
{ department: 'J', number: 's311F' },
|
||||||
|
{ department: 'J S', number: '311F' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1]));
|
||||||
|
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -73,7 +73,7 @@ const generateCourses = (count: number): Course[] => {
|
|||||||
|
|
||||||
const exampleCourses = generateCourses(numberOfCourses);
|
const exampleCourses = generateCourses(numberOfCourses);
|
||||||
|
|
||||||
type CourseWithId = Course & BaseItem;
|
type CourseWithId = { course: Course } & BaseItem;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/SortableList',
|
title: 'Components/Common/SortableList',
|
||||||
@@ -91,11 +91,10 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
draggables: exampleCourses.map(course => ({
|
draggables: exampleCourses.map(course => ({
|
||||||
id: course.uniqueId,
|
id: course.uniqueId,
|
||||||
...course,
|
course,
|
||||||
getConflicts: course.getConflicts,
|
|
||||||
})),
|
})),
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
renderItem: course => <PopupCourseBlock key={course.id} course={course} colors={course.colors} />,
|
renderItem: ({ id, course }) => <PopupCourseBlock key={id} course={course} colors={course.colors} />,
|
||||||
},
|
},
|
||||||
render: args => (
|
render: args => (
|
||||||
<div className='h-3xl w-3xl transform-none'>
|
<div className='h-3xl w-3xl transform-none'>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type { SiteSupportType } from '@views/lib/getSiteSupport';
|
|||||||
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
|
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import DialogProvider from './common/DialogProvider/DialogProvider';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
|
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
|
||||||
}
|
}
|
||||||
@@ -82,28 +84,30 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
<NewSearchLink />
|
<DialogProvider>
|
||||||
<RecruitmentBanner />
|
<NewSearchLink />
|
||||||
<TableHead>Plus</TableHead>
|
<RecruitmentBanner />
|
||||||
{rows.map(
|
<TableHead>Plus</TableHead>
|
||||||
row =>
|
{rows.map(
|
||||||
row.course && (
|
row =>
|
||||||
<TableRow
|
row.course && (
|
||||||
key={row.course.uniqueId}
|
<TableRow
|
||||||
row={row}
|
key={row.course.uniqueId}
|
||||||
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
|
row={row}
|
||||||
activeSchedule={activeSchedule}
|
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
|
||||||
onClick={handleRowButtonClick(row.course)}
|
activeSchedule={activeSchedule}
|
||||||
/>
|
onClick={handleRowButtonClick(row.course)}
|
||||||
)
|
/>
|
||||||
)}
|
)
|
||||||
<CourseCatalogInjectedPopup
|
)}
|
||||||
course={selectedCourse!} // always defined when showPopup is true
|
<CourseCatalogInjectedPopup
|
||||||
show={showPopup}
|
course={selectedCourse!} // always defined when showPopup is true
|
||||||
onClose={() => setShowPopup(false)}
|
show={showPopup}
|
||||||
afterLeave={() => setSelectedCourse(null)}
|
onClose={() => setShowPopup(false)}
|
||||||
/>
|
afterLeave={() => setSelectedCourse(null)}
|
||||||
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
/>
|
||||||
|
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
||||||
|
</DialogProvider>
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,15 +155,14 @@ export default function PopupMain(): JSX.Element {
|
|||||||
<SortableList
|
<SortableList
|
||||||
draggables={activeSchedule.courses.map(course => ({
|
draggables={activeSchedule.courses.map(course => ({
|
||||||
id: course.uniqueId,
|
id: course.uniqueId,
|
||||||
...course,
|
course,
|
||||||
getConflicts: course.getConflicts,
|
|
||||||
}))}
|
}))}
|
||||||
onChange={reordered => {
|
onChange={reordered => {
|
||||||
activeSchedule.courses = reordered.map(({ id: _id, ...course }) => course);
|
activeSchedule.courses = reordered.map(({ course }) => course);
|
||||||
replaceSchedule(getActiveSchedule(), activeSchedule);
|
replaceSchedule(getActiveSchedule(), activeSchedule);
|
||||||
}}
|
}}
|
||||||
renderItem={course => (
|
renderItem={({ id, course }) => (
|
||||||
<PopupCourseBlock key={course.id} course={course} colors={course.colors} />
|
<PopupCourseBlock key={id} course={course} colors={course.colors} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ export default function HexColorEditor({ hexCode, setHexCode }: HexColorEditorPr
|
|||||||
const tagColor = pickFontColor(previewColor.slice(1) as `#${string}`);
|
const tagColor = pickFontColor(previewColor.slice(1) as `#${string}`);
|
||||||
|
|
||||||
const [localHexCode, setLocalHexCode] = React.useState(hexCode);
|
const [localHexCode, setLocalHexCode] = React.useState(hexCode);
|
||||||
const debouncedSetHexCode = useDebounce((value: string) => setHexCode(value), 500);
|
const debouncedSetHexCode = useDebounce(setHexCode, 500);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (hexCode !== localHexCode) {
|
setLocalHexCode(hexCode);
|
||||||
setLocalHexCode(hexCode);
|
|
||||||
}
|
|
||||||
}, [hexCode]);
|
}, [hexCode]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
||||||
import { CalendarDots, Export, FilePng, Sidebar } from '@phosphor-icons/react';
|
import { CalendarDots, Export, FileCode, FilePng, Sidebar } from '@phosphor-icons/react';
|
||||||
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
|
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
|
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
|
||||||
@@ -11,7 +11,7 @@ import useSchedules from '@views/hooks/useSchedules';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { saveAsCal, saveCalAsPng } from '../utils';
|
import { handleExportJson, saveAsCal, saveCalAsPng } from '../utils';
|
||||||
|
|
||||||
interface CalendarHeaderProps {
|
interface CalendarHeaderProps {
|
||||||
sidebarOpen?: boolean;
|
sidebarOpen?: boolean;
|
||||||
@@ -98,6 +98,18 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
|||||||
Save as .cal
|
Save as .cal
|
||||||
</Button>
|
</Button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<Button
|
||||||
|
className='w-full flex justify-start'
|
||||||
|
onClick={() => handleExportJson(activeSchedule.id)}
|
||||||
|
color='ut-black'
|
||||||
|
size='small'
|
||||||
|
variant='minimal'
|
||||||
|
icon={FileCode}
|
||||||
|
>
|
||||||
|
Save as .json
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
{/* <MenuItem>
|
{/* <MenuItem>
|
||||||
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
|
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
|
||||||
Export Unique IDs
|
Export Unique IDs
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AppStoreLogo, ForkKnife, X as CloseIcon } from '@phosphor-icons/react';
|
import { AppStoreLogo, ForkKnife, X as CloseIcon } from '@phosphor-icons/react';
|
||||||
import { UT_DINING_APP_STORE_URL, UT_DINING_GOOGLE_PLAY_URL } from '@shared/util/appUrls';
|
import { UT_DINING_APP_STORE_URL } from '@shared/util/appUrls';
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ interface LinkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const links: LinkItem[] = [
|
const links: LinkItem[] = [
|
||||||
|
{
|
||||||
|
text: "Spring '26 Course Schedule",
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20262/',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "Fall '25 Course Schedule",
|
text: "Fall '25 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
|
||||||
@@ -22,10 +26,6 @@ const links: LinkItem[] = [
|
|||||||
text: "Summer '25 Course Schedule",
|
text: "Summer '25 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// text: "Spring '25 Course Schedule",
|
|
||||||
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
text: 'Course Schedule Archives',
|
text: 'Course Schedule Archives',
|
||||||
url: 'https://registrar.utexas.edu/schedules/archive',
|
url: 'https://registrar.utexas.edu/schedules/archive',
|
||||||
@@ -34,10 +34,10 @@ const links: LinkItem[] = [
|
|||||||
text: 'My Degree Audit (IDA)',
|
text: 'My Degree Audit (IDA)',
|
||||||
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
|
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// text: "'24-'25 Academic Calendar",
|
text: "'25-'26 Academic Calendar",
|
||||||
// url: 'https://registrar.utexas.edu/calendars/24-25',
|
url: 'https://registrar.utexas.edu/calendars/25-26',
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
text: 'Registration Info Sheet (RIS)',
|
text: 'Registration Info Sheet (RIS)',
|
||||||
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',
|
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { tz, TZDate } from '@date-fns/tz';
|
import { tz, TZDate } from '@date-fns/tz';
|
||||||
|
import exportSchedule from '@pages/background/lib/exportSchedule';
|
||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import type { CourseMeeting } from '@shared/types/CourseMeeting';
|
import type { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||||
@@ -261,6 +262,22 @@ export const saveAsCal = async () => {
|
|||||||
downloadBlob(icsString, 'CALENDAR', 'schedule.ics');
|
downloadBlob(icsString, 'CALENDAR', 'schedule.ics');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves current schedule to JSON that can be imported on other devices.
|
||||||
|
* @param id - Provided schedule ID to download
|
||||||
|
*/
|
||||||
|
export const handleExportJson = async (id: string) => {
|
||||||
|
const jsonString = await exportSchedule(id);
|
||||||
|
if (jsonString) {
|
||||||
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
const schedule = schedules.find(s => s.id === id);
|
||||||
|
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
||||||
|
await downloadBlob(jsonString, 'JSON', fileName);
|
||||||
|
} else {
|
||||||
|
console.error('Error exporting schedule: jsonString is undefined');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the calendar as a PNG image.
|
* Saves the calendar as a PNG image.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
color: #303030;
|
color: #303030;
|
||||||
|
|
||||||
|
// fix font-family on injected pages
|
||||||
|
* {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
|
||||||
[data-rfd-drag-handle-context-id=':r1:'] {
|
[data-rfd-drag-handle-context-id=':r1:'] {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
|||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='border border-ut-offwhite/50 rounded bg-white'>
|
<div className='max-h-[200px] flex flex-col border border-ut-offwhite/50 rounded bg-white'>
|
||||||
<Disclosure defaultOpen={defaultOpen}>
|
<Disclosure defaultOpen={defaultOpen}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
@@ -54,17 +54,17 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
|||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as='div'
|
as='div'
|
||||||
className='overflow-hidden'
|
className='flex flex-1 flex-col overflow-y-hidden'
|
||||||
enter='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
enter='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
||||||
enterFrom='max-h-0 opacity-0 p-0.5'
|
enterFrom='max-h-0 opacity-0 p-0.5'
|
||||||
enterTo='max-h-[440px] opacity-100 p-0'
|
enterTo='max-h-[200px] opacity-100 p-0'
|
||||||
leave='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
leave='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
||||||
leaveFrom='max-h-[440px] opacity-100 p-0'
|
leaveFrom='max-h-[200px] opacity-100 p-0'
|
||||||
leaveTo='max-h-0 opacity-0 p-0.5'
|
leaveTo='max-h-0 opacity-0 p-0.5'
|
||||||
>
|
>
|
||||||
<div className='px-3.5 pb-2.5 pt-2'>
|
<DisclosurePanel className='mx-1.75 mb-2.5 mt-2 flex flex-1 flex-col overflow-y-auto'>
|
||||||
<DisclosurePanel>{children}</DisclosurePanel>
|
<div className='mx-1.75'>{children}</div>
|
||||||
</div>
|
</DisclosurePanel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
*/
|
*/
|
||||||
const WHATSNEW_POPUP_VERSION = 2;
|
const WHATSNEW_POPUP_VERSION = 2;
|
||||||
|
|
||||||
const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
|
// const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
|
||||||
|
|
||||||
type Feature = {
|
type Feature = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -60,7 +60,7 @@ const NEW_FEATURES = [
|
|||||||
* @returns A JSX of WhatsNewPopupContent component.
|
* @returns A JSX of WhatsNewPopupContent component.
|
||||||
*/
|
*/
|
||||||
export default function WhatsNewPopupContent(): JSX.Element {
|
export default function WhatsNewPopupContent(): JSX.Element {
|
||||||
const [videoError, setVideoError] = useState(false);
|
const [videoError, _setVideoError] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full flex flex-row justify-between'>
|
<div className='w-full flex flex-row justify-between'>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import { validateLoginStatus } from '@shared/util/checkLoginStatus';
|
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
import useSchedules from '@views/hooks/useSchedules';
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
@@ -43,6 +42,8 @@ export default function InjectedButton(): JSX.Element | null {
|
|||||||
await addCourseByURL(activeSchedule, a);
|
await addCourseByURL(activeSchedule, a);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// We'll allow the alert for this WIP feature
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
window.alert('Logged into UT Registrar.');
|
window.alert('Logged into UT Registrar.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
|||||||
options={{
|
options={{
|
||||||
...chartOptions,
|
...chartOptions,
|
||||||
title: {
|
title: {
|
||||||
text: `There is currently no grade distribution data for ${course.department} ${course.number}`,
|
text: `There is currently no grade distribution data for ${course.department} ${course.getNumberWithoutTerm()}`,
|
||||||
},
|
},
|
||||||
tooltip: { enabled: false },
|
tooltip: { enabled: false },
|
||||||
}}
|
}}
|
||||||
@@ -228,7 +228,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
|||||||
<Text variant='small' className='text-ut-black'>
|
<Text variant='small' className='text-ut-black'>
|
||||||
Grade Distribution for{' '}
|
Grade Distribution for{' '}
|
||||||
<Text variant='small' className='font-extrabold!' as='strong'>
|
<Text variant='small' className='font-extrabold!' as='strong'>
|
||||||
{course.department} {course.number}
|
{course.department} {course.getNumberWithoutTerm()}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<select
|
<select
|
||||||
@@ -267,7 +267,8 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
|||||||
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3 text-center'>
|
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3 text-center'>
|
||||||
<Text variant='small' className='text-theme-red'>
|
<Text variant='small' className='text-theme-red'>
|
||||||
We couldn't find {semester !== 'Aggregate' && ` ${semester}`} grades for this
|
We couldn't find {semester !== 'Aggregate' && ` ${semester}`} grades for this
|
||||||
instructor, so here are the grades for all {course.department} {course.number} sections.
|
instructor, so here are the grades for all {course.department}{' '}
|
||||||
|
{course.getNumberWithoutTerm()} sections.
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import createSchedule from '@pages/background/lib/createSchedule';
|
||||||
|
import switchSchedule from '@pages/background/lib/switchSchedule';
|
||||||
import {
|
import {
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
CalendarDots,
|
CalendarDots,
|
||||||
@@ -14,8 +16,10 @@ import { background } from '@shared/messages';
|
|||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import type Instructor from '@shared/types/Instructor';
|
import type Instructor from '@shared/types/Instructor';
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import { englishStringifyList } from '@shared/util/string';
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import { Chip, coreMap, flagMap } from '@views/components/common/Chip';
|
import { Chip, coreMap, flagMap } from '@views/components/common/Chip';
|
||||||
|
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
|
||||||
import Divider from '@views/components/common/Divider';
|
import Divider from '@views/components/common/Divider';
|
||||||
import Link from '@views/components/common/Link';
|
import Link from '@views/components/common/Link';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
@@ -60,7 +64,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
|||||||
|
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
const lastCopyTime = useRef<number>(0);
|
const lastCopyTime = useRef<number>(0);
|
||||||
|
const showDialog = usePrompt();
|
||||||
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
||||||
|
|
||||||
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
@@ -112,10 +116,78 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddToNewSchedule = async (close: () => void) => {
|
||||||
|
const newScheduleId = await createSchedule(`${course.semester.season} ${course.semester.year}`);
|
||||||
|
switchSchedule(newScheduleId);
|
||||||
|
addCourse({ course, scheduleId: newScheduleId });
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddOrRemoveCourse = async () => {
|
const handleAddOrRemoveCourse = async () => {
|
||||||
|
const uniqueSemesterCodes = [
|
||||||
|
...new Set(
|
||||||
|
activeSchedule.courses
|
||||||
|
.map(course => course.semester.code)
|
||||||
|
.filter((code): code is string => code !== undefined)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
uniqueSemesterCodes.sort();
|
||||||
|
const codeToReadableMap: Record<string, string> = {};
|
||||||
|
activeSchedule.courses.forEach(course => {
|
||||||
|
const { code } = course.semester;
|
||||||
|
if (code) {
|
||||||
|
const readable = `${course.semester.season} ${course.semester.year}`;
|
||||||
|
codeToReadableMap[code] = readable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sortedSemesters = uniqueSemesterCodes
|
||||||
|
.map(code => codeToReadableMap[code])
|
||||||
|
.filter((value): value is string => value !== undefined);
|
||||||
|
const activeSemesters = englishStringifyList(sortedSemesters);
|
||||||
|
|
||||||
if (!activeSchedule) return;
|
if (!activeSchedule) return;
|
||||||
if (!courseAdded) {
|
if (!courseAdded) {
|
||||||
addCourse({ course, scheduleId: activeSchedule.id });
|
const currentSemesterCode = course.semester.code;
|
||||||
|
// Show warning if this course is for a different semester than the selected schedule
|
||||||
|
if (
|
||||||
|
activeSchedule.courses.length > 0 &&
|
||||||
|
activeSchedule.courses.every(otherCourse => otherCourse.semester.code !== currentSemesterCode)
|
||||||
|
) {
|
||||||
|
const dialogButtons = (close: () => void) => (
|
||||||
|
<>
|
||||||
|
<Button variant='minimal' color='ut-black' onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='filled'
|
||||||
|
color='ut-burntorange'
|
||||||
|
onClick={() => {
|
||||||
|
handleAddToNewSchedule(close);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start a new schedule
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
showDialog({
|
||||||
|
title: 'This course section is from a different semester!',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
The section you're adding is for{' '}
|
||||||
|
<span className='text-ut-burntorange whitespace-nowrap'>
|
||||||
|
{course.semester.season} {course.semester.year}
|
||||||
|
</span>
|
||||||
|
, but your current schedule contains sections in{' '}
|
||||||
|
<span className='text-ut-burntorange whitespace-nowrap'>{activeSemesters}</span>. Mixing
|
||||||
|
semesters in one schedule may cause confusion.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
buttons: dialogButtons,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addCourse({ course, scheduleId: activeSchedule.id });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
removeCourse({ course, scheduleId: activeSchedule.id });
|
removeCourse({ course, scheduleId: activeSchedule.id });
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/views/components/injected/SearchResultShader.tsx
Normal file
39
src/views/components/injected/SearchResultShader.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
// @TODO Get a better name for this class
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The existing search results (kws), only with alternate shading for easier readability
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default function ShadedResults(): null {
|
||||||
|
useEffect(() => {
|
||||||
|
const table = document.getElementById('kw_results_table');
|
||||||
|
if (!table) {
|
||||||
|
console.error('Results table not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
if (!tbody) {
|
||||||
|
console.error('Table tbody not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
#kw_results_table tbody tr:nth-child(even) {
|
||||||
|
background-color: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
#kw_results_table tbody tr:nth-child(even) td {
|
||||||
|
background-color: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
style.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
// import addCourse from '@pages/background/lib/addCourse';
|
// import addCourse from '@pages/background/lib/addCourse';
|
||||||
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
|
||||||
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
|
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
|
||||||
import exportSchedule from '@pages/background/lib/exportSchedule';
|
|
||||||
import importSchedule from '@pages/background/lib/importSchedule';
|
import importSchedule from '@pages/background/lib/importSchedule';
|
||||||
import { CalendarDots, Trash } from '@phosphor-icons/react';
|
import { CalendarDots, Trash } from '@phosphor-icons/react';
|
||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import { DevStore } from '@shared/storage/DevStore';
|
import { DevStore } from '@shared/storage/DevStore';
|
||||||
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
|
||||||
import { CRX_PAGES } from '@shared/types/CRXPages';
|
import { CRX_PAGES } from '@shared/types/CRXPages';
|
||||||
import MIMEType from '@shared/types/MIMEType';
|
import MIMEType from '@shared/types/MIMEType';
|
||||||
import { downloadBlob } from '@shared/util/downloadBlob';
|
|
||||||
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
||||||
// import { getCourseColors } from '@shared/util/colors';
|
// import { getCourseColors } from '@shared/util/colors';
|
||||||
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
||||||
@@ -32,6 +29,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import IconoirGitFork from '~icons/iconoir/git-fork';
|
import IconoirGitFork from '~icons/iconoir/git-fork';
|
||||||
|
|
||||||
|
import { handleExportJson } from '../calendar/utils';
|
||||||
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
|
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
|
||||||
import FileUpload from '../common/FileUpload';
|
import FileUpload from '../common/FileUpload';
|
||||||
import { useMigrationDialog } from '../common/MigrationDialog';
|
import { useMigrationDialog } from '../common/MigrationDialog';
|
||||||
@@ -232,18 +230,6 @@ export default function Settings(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportClick = async (id: string) => {
|
|
||||||
const jsonString = await exportSchedule(id);
|
|
||||||
if (jsonString) {
|
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
|
||||||
const schedule = schedules.find(s => s.id === id);
|
|
||||||
const fileName = `${schedule?.name ?? `schedule_${id}`}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
|
||||||
await downloadBlob(jsonString, 'JSON', fileName);
|
|
||||||
} else {
|
|
||||||
console.error('Error exporting schedule: jsonString is undefined');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -400,7 +386,7 @@ export default function Settings(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='ut-burntorange'
|
color='ut-burntorange'
|
||||||
onClick={() => handleExportClick(activeSchedule.id)}
|
onClick={() => handleExportJson(activeSchedule.id)}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
|
|||||||
import { useDialog } from '@views/contexts/DialogContext';
|
import { useDialog } from '@views/contexts/DialogContext';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { LogoIcon } from '../components/common/LogoIcon';
|
// import useChangelog from './useChangelog';
|
||||||
import useChangelog from './useChangelog';
|
|
||||||
|
|
||||||
const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
|
const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
|
||||||
|
|
||||||
@@ -17,8 +16,8 @@ const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
|
|||||||
*/
|
*/
|
||||||
export default function useWhatsNewPopUp(): () => void {
|
export default function useWhatsNewPopUp(): () => void {
|
||||||
const showDialog = useDialog();
|
const showDialog = useDialog();
|
||||||
const showChangeLog = useChangelog();
|
// const showChangeLog = useChangelog();
|
||||||
const { version } = chrome.runtime.getManifest();
|
// const { version } = chrome.runtime.getManifest();
|
||||||
|
|
||||||
const showPopUp = () => {
|
const showPopUp = () => {
|
||||||
showDialog(close => ({
|
showDialog(close => ({
|
||||||
|
|||||||
34
src/views/lib/CourseCatalogScraper.test.ts
Normal file
34
src/views/lib/CourseCatalogScraper.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { CourseCatalogScraper } from './CourseCatalogScraper';
|
||||||
|
|
||||||
|
describe('CourseCatalogScraper::separateCourseName', () => {
|
||||||
|
it('should separate a simple course', () => {
|
||||||
|
// UT Formats strings weird... lots of meaningless spaces
|
||||||
|
const input = 'C S 314H DATA STRUCTURES: HONORS ';
|
||||||
|
const expected = ['DATA STRUCTURES: HONORS', 'C S', '314H'];
|
||||||
|
const result = CourseCatalogScraper.separateCourseName(input);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('separate summer courses ', () => {
|
||||||
|
// UT Formats strings weird... lots of meaningless spaces
|
||||||
|
const inputs = [
|
||||||
|
'P R f378 PUBLIC RELATNS TECHNIQUES-IRL (First term) ',
|
||||||
|
'CRP s396 INDEPENDENT RESEARCH IN CRP (Second term) ',
|
||||||
|
'B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term) ',
|
||||||
|
'J w379 JOURNALISM INDEPENDENT STUDY (Whole term) ',
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
['PUBLIC RELATNS TECHNIQUES-IRL (First term)', 'P R', 'f378'],
|
||||||
|
['INDEPENDENT RESEARCH IN CRP (Second term)', 'CRP', 's396'],
|
||||||
|
['1-MANAGERIAL MICROECON-I-DAL (Nine week term)', 'B A', 'n284S'],
|
||||||
|
['JOURNALISM INDEPENDENT STUDY (Whole term)', 'J', 'w379'],
|
||||||
|
];
|
||||||
|
const results = inputs.map(input => CourseCatalogScraper.separateCourseName(input));
|
||||||
|
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,7 +75,7 @@ export class CourseCatalogScraper {
|
|||||||
|
|
||||||
fullName = fullName.replace(/\s\s+/g, ' ').trim();
|
fullName = fullName.replace(/\s\s+/g, ' ').trim();
|
||||||
|
|
||||||
const [courseName, department, number] = this.separateCourseName(fullName);
|
const [courseName, department, number] = CourseCatalogScraper.separateCourseName(fullName);
|
||||||
const [status, isReserved] = this.getStatus(row);
|
const [status, isReserved] = this.getStatus(row);
|
||||||
|
|
||||||
const newCourse = new Course({
|
const newCourse = new Course({
|
||||||
@@ -113,16 +113,31 @@ export class CourseCatalogScraper {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```
|
* ```
|
||||||
* separateCourseName("CS 314H - Honors Discrete Structures") => ["Honors Discrete Structures", "CS", "314H"]
|
* separateCourseName("C S 314H DATA STRUCTURES: HONORS") => ["DATA STRUCTURES: HONORS", "C S", "314H"]
|
||||||
* ```
|
* ```
|
||||||
* @param courseFullName - the full name of the course (e.g. "CS 314H - Honors Discrete Structures")
|
* @param courseFullName - the full name of the course (e.g. "C S 314H DATA STRUCTURES: HONORS")
|
||||||
* @returns an array of the course name , department, and number
|
* @returns an array of the course name, department, and number
|
||||||
*/
|
*/
|
||||||
separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
static separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
||||||
let courseNumberIndex = courseFullName.search(/\d/);
|
// C S 314H DATA STRUCTURES: HONORS
|
||||||
let department = courseFullName.substring(0, courseNumberIndex).trim();
|
// ^ Here for normal courses
|
||||||
let number = courseFullName.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
// B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term)
|
||||||
let courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
// ^ Also works for summer courses ([f]irst term, [s]econd term, [n]ine week term, [w]hole term)
|
||||||
|
const courseNumberIndex = courseFullName.search(/\w?\d/);
|
||||||
|
|
||||||
|
if (courseNumberIndex === -1) {
|
||||||
|
throw new Error("Course name doesn't have a course number");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything before the course number
|
||||||
|
const department = courseFullName.substring(0, courseNumberIndex).trim();
|
||||||
|
|
||||||
|
const number = courseFullName
|
||||||
|
.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex))
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Everything after the course number
|
||||||
|
const courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
||||||
|
|
||||||
return [courseName, department, number];
|
return [courseName, department, number];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,16 +109,22 @@ function generateQuery(
|
|||||||
includeInstructor: boolean
|
includeInstructor: boolean
|
||||||
): [string, GradeDistributionParams] {
|
): [string, GradeDistributionParams] {
|
||||||
const query = `
|
const query = `
|
||||||
select * from grade_distributions
|
SELECT * FROM grade_distributions
|
||||||
where Department_Code = :department_code
|
WHERE Department_Code = :department_code
|
||||||
and Course_Number = :course_number
|
AND Course_Number COLLATE NOCASE IN (
|
||||||
${includeInstructor ? `and Instructor_Last = :instructor_last collate nocase` : ''}
|
:course_number,
|
||||||
${semester ? `and Semester = :semester` : ''}
|
concat('F', :course_number), -- Check summer courses with prefix, too
|
||||||
|
concat('S', :course_number),
|
||||||
|
concat('N', :course_number),
|
||||||
|
concat('W', :course_number)
|
||||||
|
)
|
||||||
|
${includeInstructor ? `AND Instructor_Last = :instructor_last COLLATE NOCASE` : ''}
|
||||||
|
${semester ? `AND Semester = :semester` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params: GradeDistributionParams = {
|
const params: GradeDistributionParams = {
|
||||||
':department_code': course.department,
|
':department_code': course.department,
|
||||||
':course_number': course.number,
|
':course_number': course.getNumberWithoutTerm(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeInstructor) {
|
if (includeInstructor) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const SiteSupport = {
|
|||||||
MY_UT: 'MY_UT',
|
MY_UT: 'MY_UT',
|
||||||
COURSE_CATALOG_SEARCH: 'COURSE_CATALOG_SEARCH',
|
COURSE_CATALOG_SEARCH: 'COURSE_CATALOG_SEARCH',
|
||||||
CLASSLIST: 'CLASSLIST',
|
CLASSLIST: 'CLASSLIST',
|
||||||
|
COURSE_CATALOG_KWS: 'COURSE_CATALOG_KWS',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,9 @@ export default function getSiteSupport(url: string): SiteSupportType | null {
|
|||||||
return SiteSupport.UT_PLANNER;
|
return SiteSupport.UT_PLANNER;
|
||||||
}
|
}
|
||||||
if (url.includes('utdirect.utexas.edu/apps/registrar/course_schedule')) {
|
if (url.includes('utdirect.utexas.edu/apps/registrar/course_schedule')) {
|
||||||
|
if (url.includes('kws_results')) {
|
||||||
|
return SiteSupport.COURSE_CATALOG_KWS;
|
||||||
|
}
|
||||||
if (url.includes('results')) {
|
if (url.includes('results')) {
|
||||||
return SiteSupport.COURSE_CATALOG_LIST;
|
return SiteSupport.COURSE_CATALOG_LIST;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import presetUno from '@unocss/preset-uno';
|
import presetWind4 from '@unocss/preset-wind4';
|
||||||
import presetWebFonts from '@unocss/preset-web-fonts';
|
import presetWebFonts from '@unocss/preset-web-fonts';
|
||||||
import transformerDirectives from '@unocss/transformer-directives';
|
import transformerDirectives from '@unocss/transformer-directives';
|
||||||
import transformerVariantGroup from '@unocss/transformer-variant-group';
|
import transformerVariantGroup from '@unocss/transformer-variant-group';
|
||||||
@@ -50,7 +50,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
presets: [
|
presets: [
|
||||||
presetUno(),
|
presetWind4(),
|
||||||
|
// todo: for some reason, breaking eslint ._.
|
||||||
presetWebFonts({
|
presetWebFonts({
|
||||||
provider: 'none',
|
provider: 'none',
|
||||||
fonts: {
|
fonts: {
|
||||||
|
|||||||
@@ -207,6 +207,9 @@ export default defineConfig({
|
|||||||
hmr: {
|
hmr: {
|
||||||
clientPort: 5173,
|
clientPort: 5173,
|
||||||
},
|
},
|
||||||
|
cors: {
|
||||||
|
origin: [/chrome-extension:\/\//],
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/debug.html': {
|
'/debug.html': {
|
||||||
target: 'http://localhost:5173',
|
target: 'http://localhost:5173',
|
||||||
@@ -256,16 +259,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
// test: {
|
||||||
coverage: {
|
// coverage: {
|
||||||
provider: 'v8',
|
// provider: "v8",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
css: {
|
// css: {
|
||||||
preprocessorOptions: {
|
// preprocessorOptions: {
|
||||||
scss: {
|
// scss: {
|
||||||
api: 'modern-compiler',
|
// api: "modern-compiler",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user