Compare commits

..

1 Commits

Author SHA1 Message Date
a423c6ed4e feat: initial commit bs code 2025-06-15 20:41:37 -05:00
29 changed files with 1186 additions and 3647 deletions

View File

@@ -1,3 +0,0 @@
SENTRY_ORG=longhorn-developers
SENTRY_PROJECT=ut-registration-plus
SENTRY_AUTH_TOKEN=

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1754725699, "lastModified": 1744932701,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=", "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054", "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,28 +1,24 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
}; };
outputs = outputs =
{ inputs:
self, inputs.flake-utils.lib.eachDefaultSystem (
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system: system:
let let
pkgs = (import nixpkgs { inherit system; }); pkgs = (import (inputs.nixpkgs) { inherit system; });
in in
{ {
formatter = pkgs.nixfmt-rfc-style; formatter = pkgs.nixfmt-rfc-style;
devShells.default = pkgs.mkShell { devShell = pkgs.mkShell {
name = "utrp-dev";
buildInputs = with pkgs; [ buildInputs = with pkgs; [
nodejs_20 # v20.19.4 nodejs_20 # v20.19.0
pnpm_10 # v10.14.0 pnpm_10 # v10.8.1
just
]; ];
shellHook = '' shellHook = ''

View File

@@ -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": "^66.5.1", "@unocss/vite": "^0.63.6",
"@vitejs/plugin-react": "^5.0.2", "@vitejs/plugin-react": "^4.3.4",
"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.2.0", "@crxjs/vite-plugin": "2.0.0-beta.21",
"@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.18.0", "@types/node": "^22.13.5",
"@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": "^66.5.1", "@unocss/eslint-config": "^0.63.6",
"@unocss/postcss": "^66.5.1", "@unocss/postcss": "^0.63.6",
"@unocss/preset-web-fonts": "^66.5.1", "@unocss/preset-uno": "^0.63.6",
"@unocss/preset-wind4": "^66.5.1", "@unocss/preset-web-fonts": "^0.63.6",
"@unocss/reset": "^66.5.1", "@unocss/reset": "^0.63.6",
"@unocss/transformer-directives": "^66.5.1", "@unocss/transformer-directives": "^0.63.6",
"@unocss/transformer-variant-group": "^66.5.1", "@unocss/transformer-variant-group": "^0.63.6",
"@vitejs/plugin-react-swc": "^4.0.1", "@vitejs/plugin-react-swc": "^3.8.0",
"@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": "^66.5.1", "unocss": "^0.63.6",
"unocss-preset-primitives": "0.0.2-beta.2", "unocss-preset-primitives": "0.0.2-beta.1",
"unplugin-icons": "^0.19.3", "unplugin-icons": "^0.19.3",
"vite": "^7.1.5", "vite": "^5.4.14",
"vite-plugin-inspect": "^11.3.3", "vite-plugin-inspect": "^0.8.9",
"vitest": "^2.1.9" "vitest": "^2.1.9"
}, },
"engineStrict": true, "engineStrict": true,
@@ -154,15 +154,15 @@
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"@unocss/vite": "patches/@unocss__vite.patch", "@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch",
"@crxjs/vite-plugin": "patches/@crxjs__vite-plugin.patch" "@unocss/vite": "patches/@unocss__vite.patch"
}, },
"onlyBuiltDependencies": [ "overrides": {
"@swc/core" "es-module-lexer": "^1.5.4"
] }
}, },
"volta": { "volta": {
"node": "20.19.4", "node": "20.9.0",
"pnpm": "10.14.0" "pnpm": "10.6.5"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
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",

View File

@@ -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 3b94c870f00ce7e70be208199db48a3bd5569830..4aaaec7d5b656f1c4066c6295d5228d746420d17 100644 index 7210f5fd650a0b7bb36b467fff85feb0d8e4ec63..c8f98bd314daec0b91c514ea9d9fc2b79cea8502 100644
--- a/dist/index.mjs --- a/dist/index.mjs
+++ b/dist/index.mjs +++ b/dist/index.mjs
@@ -24,8 +24,8 @@ const VIRTUAL_ENTRY_ALIAS = [ @@ -369,15 +369,15 @@ const VIRTUAL_ENTRY_ALIAS = [
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/ /^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
]; ];
const LAYER_MARK_ALL = "__ALL__"; const LAYER_MARK_ALL = "__ALL__";
@@ -10,19 +10,27 @@ index 3b94c870f00ce7e70be208199db48a3bd5569830..4aaaec7d5b656f1c4066c6295d5228d7
-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) {
const defaultPipelineExclude = [cssIdRE]; if (id.match(RESOLVED_ID_WITH_QUERY_RE))
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|vine.ts|mdx?|astro|elm|php|phtml|html)($|\?)/]; return id;
@@ -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) {
- 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"; + return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
virtual += match[2] || ""; }
if (importer) }
virtual = resolve$1(importer, "..", virtual); }
@@ -813,7 +813,7 @@ function GlobalModeDevPlugin(ctx) { @@ -652,7 +652,7 @@ function GlobalModeBuildPlugin(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
@@ -31,7 +39,7 @@ index 3b94c870f00ce7e70be208199db48a3bd5569830..4aaaec7d5b656f1c4066c6295d5228d7
map: { mappings: "" } map: { mappings: "" }
}; };
}, },
@@ -832,7 +832,7 @@ function GlobalModeDevPlugin(ctx) { @@ -933,7 +933,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
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

File diff suppressed because it is too large Load Diff

View File

@@ -38,12 +38,7 @@ 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: { default_icon: `icons/icon_${mode}_32.png`,
'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`,

View File

@@ -1,7 +1,6 @@
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';
@@ -10,8 +9,9 @@ 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,7 +30,3 @@ 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);
}

View File

@@ -15,8 +15,6 @@ 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'>;
} }
@@ -84,30 +82,28 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
return ( return (
<ExtensionRoot> <ExtensionRoot>
<DialogProvider> <NewSearchLink />
<NewSearchLink /> <RecruitmentBanner />
<RecruitmentBanner /> <TableHead>Plus</TableHead>
<TableHead>Plus</TableHead> {rows.map(
{rows.map( row =>
row => row.course && (
row.course && ( <TableRow
<TableRow key={row.course.uniqueId}
key={row.course.uniqueId} row={row}
row={row} isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
isSelected={row.course.uniqueId === selectedCourse?.uniqueId} activeSchedule={activeSchedule}
activeSchedule={activeSchedule} onClick={handleRowButtonClick(row.course)}
onClick={handleRowButtonClick(row.course)} />
/> )
) )}
)} <CourseCatalogInjectedPopup
<CourseCatalogInjectedPopup course={selectedCourse!} // always defined when showPopup is true
course={selectedCourse!} // always defined when showPopup is true show={showPopup}
show={showPopup} onClose={() => setShowPopup(false)}
onClose={() => setShowPopup(false)} afterLeave={() => setSelectedCourse(null)}
afterLeave={() => setSelectedCourse(null)} />
/> {enableScrollToLoad && <AutoLoad addRows={addRows} />}
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
</DialogProvider>
</ExtensionRoot> </ExtensionRoot>
); );
} }

View File

@@ -27,10 +27,12 @@ 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(setHexCode, 500); const debouncedSetHexCode = useDebounce((value: string) => setHexCode(value), 500);
React.useEffect(() => { React.useEffect(() => {
setLocalHexCode(hexCode); if (hexCode !== localHexCode) {
setLocalHexCode(hexCode);
}
}, [hexCode]); }, [hexCode]);
React.useEffect(() => { React.useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'; import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import { CalendarDots, Export, FileCode, FilePng, Sidebar } from '@phosphor-icons/react'; import { CalendarDots, Export, 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 { handleExportJson, saveAsCal, saveCalAsPng } from '../utils'; import { saveAsCal, saveCalAsPng } from '../utils';
interface CalendarHeaderProps { interface CalendarHeaderProps {
sidebarOpen?: boolean; sidebarOpen?: boolean;
@@ -98,18 +98,6 @@ 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

View File

@@ -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 } from '@shared/util/appUrls'; import { UT_DINING_APP_STORE_URL, UT_DINING_GOOGLE_PLAY_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';

View File

@@ -14,10 +14,6 @@ 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/',
@@ -26,6 +22,10 @@ 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: "'25-'26 Academic Calendar", // text: "'24-'25 Academic Calendar",
url: 'https://registrar.utexas.edu/calendars/25-26', // url: 'https://registrar.utexas.edu/calendars/24-25',
}, // },
{ {
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',

View File

@@ -1,5 +1,4 @@
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';
@@ -262,22 +261,6 @@ 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.
* *

View File

@@ -15,11 +15,6 @@
@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;
} }

View File

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

View File

@@ -1,5 +1,6 @@
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';
@@ -42,8 +43,6 @@ 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.');
} }
}; };

View File

@@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
function getCourseSections() {
const table = document.querySelector('table');
if (!table) return [];
const rows = Array.from(table.querySelectorAll('tr'));
const sections: { header: HTMLTableRowElement; children: HTMLTableRowElement[] }[] = [];
let currentSection: { header: HTMLTableRowElement; children: HTMLTableRowElement[] } | null = null;
for (const row of rows) {
const headerCell = row.querySelector('td.course_header');
if (headerCell) {
if (currentSection) sections.push(currentSection);
currentSection = { header: row, children: [] };
} else if (currentSection) {
currentSection.children.push(row);
}
}
if (currentSection) sections.push(currentSection);
return sections;
}
const CollapsibleSection: React.FC<{
header: HTMLTableRowElement;
childrenRows: HTMLTableRowElement[];
}> = ({ header, childrenRows }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
// Hide children rows initially
childrenRows.forEach(row => (row.style.display = open ? '' : 'none'));
// Clean up on unmount
return () => {
childrenRows.forEach(row => (row.style.display = ''));
};
}, [open, childrenRows]);
// Inject a button into the header cell
useEffect(() => {
const cell = header.querySelector('td.course_header');
if (!cell) return;
let button = cell.querySelector('.utrp-collapse-btn') as HTMLButtonElement | null;
if (!button) {
button = document.createElement('button');
button.className = 'utrp-collapse-btn';
button.style.marginRight = '8px';
cell.prepend(button);
}
button.textContent = open ? '▼' : '►';
button.onclick = () => setOpen(o => !o);
// Clean up
return () => {
button?.remove();
};
}, [header, open]);
return null;
};
export default

View File

@@ -1,5 +1,3 @@
import createSchedule from '@pages/background/lib/createSchedule';
import switchSchedule from '@pages/background/lib/switchSchedule';
import { import {
ArrowUpRight, ArrowUpRight,
CalendarDots, CalendarDots,
@@ -16,10 +14,8 @@ 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';
@@ -64,7 +60,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>) => {
@@ -116,78 +112,10 @@ 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) {
const currentSemesterCode = course.semester.code; addCourse({ course, scheduleId: activeSchedule.id });
// 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&apos;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 });
} }

View File

@@ -1,39 +0,0 @@
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;
}

View File

@@ -1,13 +1,16 @@
// 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';
@@ -29,7 +32,6 @@ 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';
@@ -230,6 +232,18 @@ 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) {
@@ -386,7 +400,7 @@ export default function Settings(): JSX.Element {
<Button <Button
variant='outline' variant='outline'
color='ut-burntorange' color='ut-burntorange'
onClick={() => handleExportJson(activeSchedule.id)} onClick={() => handleExportClick(activeSchedule.id)}
> >
Export Export
</Button> </Button>

View File

@@ -5,7 +5,8 @@ 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 useChangelog from './useChangelog'; import { LogoIcon } from '../components/common/LogoIcon';
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;
@@ -16,8 +17,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 => ({

View File

@@ -15,7 +15,6 @@ 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;
/** /**
@@ -41,9 +40,6 @@ 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;
} }

View File

@@ -1,4 +1,4 @@
import presetWind4 from '@unocss/preset-wind4'; import presetUno from '@unocss/preset-uno';
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,8 +50,7 @@ export default defineConfig({
}, },
], ],
presets: [ presets: [
presetWind4(), presetUno(),
// todo: for some reason, breaking eslint ._.
presetWebFonts({ presetWebFonts({
provider: 'none', provider: 'none',
fonts: { fonts: {

View File

@@ -207,9 +207,6 @@ 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',
@@ -259,16 +256,16 @@ export default defineConfig({
}, },
}, },
}, },
// test: { test: {
// coverage: { coverage: {
// provider: "v8", provider: 'v8',
// }, },
// }, },
// css: { css: {
// preprocessorOptions: { preprocessorOptions: {
// scss: { scss: {
// api: "modern-compiler", api: 'modern-compiler',
// }, },
// }, },
// }, },
}); });