Compare commits
21 Commits
derek/coll
...
sgunter/fe
| Author | SHA1 | Date | |
|---|---|---|---|
| bb4fbc3700 | |||
|
|
e8a8b8e1ae | ||
|
|
c21cbd77f0 | ||
| 99a035e29d | |||
|
|
64baa6d290 | ||
|
|
46fe591fa7 | ||
|
|
8f7e1bc0af | ||
|
|
9fc1098ef7 | ||
|
|
ae094416fc | ||
|
|
35d903e7c8 | ||
|
|
2e7dac1e3e | ||
|
|
7bea23a655 | ||
|
|
3d28869e92 | ||
|
|
1fffb3c2e7 | ||
|
|
f0f1f0b365 | ||
|
|
4590a74896 | ||
| be861b823c | |||
|
|
95de8df372 | ||
| 190d1db2fa | |||
| 5994ded8be | |||
|
|
b8a44b45b8 |
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=
|
||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,9 +1,27 @@
|
|||||||
|
## [2.2.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.1...v2.2.2) (2025-10-13)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add nix flake ([#593](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/593)) ([7b401ad](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7b401add1565ff401bad99745ff9e53b9a7f899f))
|
||||||
|
* automatically select new or duplicated schedules ([#583](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/583)) ([#589](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/589)) ([2a50f55](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2a50f5580d3dbeb0d66546c23cf29bbb37d80da2))
|
||||||
|
* **env:** add SENTRY env vars ([8f7e1bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8f7e1bc0af6336549068e02b80df21d4e8f4ef9c))
|
||||||
|
* export schedule button add to calendar ([#594](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/594)) ([5994ded](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5994ded8be876cb55174d27d3fdb0832b21a0ff9))
|
||||||
|
* search result shading ([#617](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/617)) ([be861b8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be861b823cb2cb7f6f4a1f266351eec3fc1c2f99))
|
||||||
|
* show warning for courses of different semesters ([#570](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/570)) ([2e7dac1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2e7dac1e3eba757231ac07ac966231c08c703a16))
|
||||||
|
* support summer grades, fix summer course parser ([#596](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/596)) ([2d92dd4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d92dd47f00a44b7d48e92a8ffba94480e4e73f9))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix or ignore various eslint warning ([#609](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/609)) ([95de8df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/95de8df37243b6d59625df515a60442f11b7a9d3))
|
||||||
|
* limit height of schedule list dropdown in the extension popup ([#543](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/543)) ([eb8141e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/eb8141ee8c3d32bce901457178d50781b78f86dd))
|
||||||
|
* whitespace wrapping in semester warning ([#629](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/629)) ([46fe591](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/46fe591fa72ef017eea7cfb8aa37d12d8f223926))
|
||||||
## [2.2.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.0...v2.2.1) (2025-06-04)
|
## [2.2.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.0...v2.2.1) (2025-06-04)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* add dining app promo ([#598](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/598)) ([be1dccf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be1dccfcb9d052c6b291b50cc53418d6bb645beb))
|
* add dining app promo ([#598](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/598)) ([be1dccf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be1dccfcb9d052c6b291b50cc53418d6bb645beb))
|
||||||
* inside jokes005 ([#590](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/590)) ([37471ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/37471efb740c7a5828cf3b54bac70954694359d7))
|
* inside jokes005 ([#590](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/590)) ([37471ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/37471efb740c7a5828cf3b54bac70954694359d7))
|
||||||
|
* **release:** v2.2.1 ([234f3d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/234f3d627d603adf8555b4d0e93106d198918169))
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|||||||
0
docs/WebSocket-Implementation-Tutorial.md
Normal file
0
docs/WebSocket-Implementation-Tutorial.md
Normal file
6
flake.lock
generated
6
flake.lock
generated
@@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1744932701,
|
"lastModified": 1759831965,
|
||||||
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
|
"narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
|
"rev": "c9b6fb798541223bbb396d287d16f43520250518",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
41
flake.nix
41
flake.nix
@@ -1,31 +1,42 @@
|
|||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
inputs:
|
{
|
||||||
inputs.flake-utils.lib.eachDefaultSystem (
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
pkgs = (import (inputs.nixpkgs) { inherit system; });
|
pkgs = (import nixpkgs { inherit system; });
|
||||||
|
|
||||||
|
commonPackages = with pkgs; [
|
||||||
|
nodejs_20 # v20.19.5
|
||||||
|
pnpm_10 # v10.18.0
|
||||||
|
];
|
||||||
|
|
||||||
|
additionalPackages = with pkgs; [
|
||||||
|
bun
|
||||||
|
nodePackages.conventional-changelog-cli
|
||||||
|
sentry-cli
|
||||||
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
formatter = pkgs.nixfmt-rfc-style;
|
formatter = pkgs.nixfmt-rfc-style;
|
||||||
|
|
||||||
devShell = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
name = "utrp-dev";
|
||||||
nodejs_20 # v20.19.0
|
buildInputs = commonPackages;
|
||||||
pnpm_10 # v10.8.1
|
};
|
||||||
just
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
devShells.full = pkgs.mkShell {
|
||||||
echo "UTRP Nix Flake Environment Loaded"
|
name = "utrp-dev-full";
|
||||||
echo "Node: $(node --version)"
|
buildInputs = commonPackages ++ additionalPackages;
|
||||||
echo "pnpm: $(pnpm --version)"
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ut-registration-plus",
|
"name": "ut-registration-plus",
|
||||||
"displayName": "UT Registration Plus",
|
"displayName": "UT Registration Plus",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
|
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
|
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"unocss": "^0.63.6",
|
"unocss": "^0.63.6",
|
||||||
"unocss-preset-primitives": "0.0.2-beta.1",
|
"unocss-preset-primitives": "0.0.2-beta.1",
|
||||||
"unplugin-icons": "^0.19.3",
|
"unplugin-icons": "^0.19.3",
|
||||||
"vite": "^5.4.14",
|
"vite": "^5.4.20",
|
||||||
"vite-plugin-inspect": "^0.8.9",
|
"vite-plugin-inspect": "^0.8.9",
|
||||||
"vitest": "^2.1.9"
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.9.0",
|
"node": "20.19.4",
|
||||||
"pnpm": "10.6.5"
|
"pnpm": "10.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
935
pnpm-lock.yaml
generated
935
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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';
|
||||||
@@ -30,3 +31,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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
import ReportIssueMain from '@views/components/ReportIssueMain';
|
import ReportIssueMain from '@views/components/ReportIssueMain';
|
||||||
import SentryProvider from '@views/contexts/SentryContext';
|
import SentryProvider from '@views/contexts/SentryContext';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -5,6 +6,8 @@ import { createRoot } from 'react-dom/client';
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<SentryProvider fullInit>
|
<SentryProvider fullInit>
|
||||||
|
<ExtensionRoot>
|
||||||
<ReportIssueMain />
|
<ReportIssueMain />
|
||||||
|
</ExtensionRoot>
|
||||||
</SentryProvider>
|
</SentryProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export interface IOptionsStore {
|
|||||||
|
|
||||||
/** whether the promo should be shown */
|
/** whether the promo should be shown */
|
||||||
showUTDiningPromo: boolean;
|
showUTDiningPromo: boolean;
|
||||||
|
|
||||||
|
/** whether the user's email address should be remembered by the extension */
|
||||||
|
rememberMyEmail: boolean;
|
||||||
|
|
||||||
|
/** the user's email address, if set and chosen to be remembered */
|
||||||
|
emailAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionsStore = createSyncStore<IOptionsStore>({
|
export const OptionsStore = createSyncStore<IOptionsStore>({
|
||||||
@@ -34,6 +40,8 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
|
|||||||
alwaysOpenCalendarInNewTab: false,
|
alwaysOpenCalendarInNewTab: false,
|
||||||
showCalendarSidebar: true,
|
showCalendarSidebar: true,
|
||||||
showUTDiningPromo: true,
|
showUTDiningPromo: true,
|
||||||
|
rememberMyEmail: false,
|
||||||
|
emailAddress: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +58,8 @@ export const initSettings = async () =>
|
|||||||
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
|
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
|
||||||
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
|
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
|
||||||
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
|
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
|
||||||
|
rememberMyEmail: await OptionsStore.get('rememberMyEmail'),
|
||||||
|
emailAddress: await OptionsStore.get('emailAddress'),
|
||||||
}) satisfies IOptionsStore;
|
}) satisfies IOptionsStore;
|
||||||
|
|
||||||
// Clothing retailer right
|
// Clothing retailer right
|
||||||
|
|||||||
@@ -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,6 +84,7 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
|
<DialogProvider>
|
||||||
<NewSearchLink />
|
<NewSearchLink />
|
||||||
<RecruitmentBanner />
|
<RecruitmentBanner />
|
||||||
<TableHead>Plus</TableHead>
|
<TableHead>Plus</TableHead>
|
||||||
@@ -104,6 +107,7 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
|
|||||||
afterLeave={() => setSelectedCourse(null)}
|
afterLeave={() => setSelectedCourse(null)}
|
||||||
/>
|
/>
|
||||||
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
{enableScrollToLoad && <AutoLoad addRows={addRows} />}
|
||||||
|
</DialogProvider>
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
|
|
||||||
import { captureFeedback } from '@sentry/react';
|
import { captureFeedback } from '@sentry/react';
|
||||||
|
import { OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Button } from './common/Button';
|
import { Button } from './common/Button';
|
||||||
@@ -12,19 +14,57 @@ import Text from './common/Text/Text';
|
|||||||
* @returns The rendered component.
|
* @returns The rendered component.
|
||||||
*/
|
*/
|
||||||
export default function ReportIssueMain(): JSX.Element {
|
export default function ReportIssueMain(): JSX.Element {
|
||||||
const [email, setEmail] = useState('');
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: emailAddress } = useQuery({
|
||||||
|
queryKey: ['settings', 'emailAddress'],
|
||||||
|
queryFn: () => OptionsStore.get('emailAddress'),
|
||||||
|
staleTime: Infinity, // Prevent loading state on refocus
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: setEmailAddress } = useMutation({
|
||||||
|
mutationKey: ['settings', 'emailAddress'],
|
||||||
|
mutationFn: async ({ rememberMyEmail, emailAddress }: { rememberMyEmail: boolean; emailAddress: string }) => {
|
||||||
|
queryClient.setQueryData(['settings', 'emailAddress'], emailAddress);
|
||||||
|
if (rememberMyEmail) {
|
||||||
|
OptionsStore.set('emailAddress', emailAddress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: rememberMyEmail } = useQuery({
|
||||||
|
queryKey: ['settings', 'rememberMyEmail'],
|
||||||
|
queryFn: () => OptionsStore.get('rememberMyEmail'),
|
||||||
|
staleTime: Infinity, // Prevent loading state on refocus
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: setRememberMyEmail } = useMutation({
|
||||||
|
mutationKey: ['settings', 'rememberMyEmail'],
|
||||||
|
mutationFn: async ({ rememberMyEmail, emailAddress }: { rememberMyEmail: boolean; emailAddress: string }) => {
|
||||||
|
queryClient.setQueryData(['settings', 'rememberMyEmail'], rememberMyEmail);
|
||||||
|
OptionsStore.set('rememberMyEmail', rememberMyEmail);
|
||||||
|
|
||||||
|
if (rememberMyEmail) {
|
||||||
|
OptionsStore.set('emailAddress', emailAddress);
|
||||||
|
} else {
|
||||||
|
OptionsStore.set('emailAddress', '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [feedback, setFeedback] = useState('');
|
const [feedback, setFeedback] = useState('');
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
const submitFeedback = async () => {
|
const submitFeedback = async () => {
|
||||||
if (!email || !feedback) {
|
if (!emailAddress || !feedback) {
|
||||||
throw new Error('Email and feedback are required');
|
throw new Error('Email and feedback are required');
|
||||||
}
|
}
|
||||||
// Here you would typically send the feedback to a server
|
|
||||||
await captureFeedback(
|
// Send the feedback to Sentry
|
||||||
|
captureFeedback(
|
||||||
{
|
{
|
||||||
message: feedback || 'No feedback provided',
|
message: feedback || 'No feedback provided',
|
||||||
email,
|
email: emailAddress,
|
||||||
tags: {
|
tags: {
|
||||||
version: chrome.runtime.getManifest().version,
|
version: chrome.runtime.getManifest().version,
|
||||||
},
|
},
|
||||||
@@ -34,16 +74,14 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset form fields and close the dialog
|
// Close the dialog
|
||||||
setEmail('');
|
|
||||||
setFeedback('');
|
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSubmitted) {
|
if (isSubmitted) {
|
||||||
return (
|
return (
|
||||||
<div className='w-80 flex flex-col rounded-lg bg-white p-6 shadow-lg'>
|
<div className='w-92 flex flex-col rounded-lg bg-white p-6 shadow-lg'>
|
||||||
<Text variant='h2' className='mb-4'>
|
<Text variant='h2' className='my-4'>
|
||||||
Thank you
|
Thank you
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant='p' className='mb-6'>
|
<Text variant='p' className='mb-6'>
|
||||||
@@ -56,28 +94,13 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSubmitted) {
|
|
||||||
return (
|
return (
|
||||||
<div className='w-80 bg-white p-6'>
|
<div className='w-92 bg-white p-6'>
|
||||||
<h2 className='mb-4 text-2xl text-orange font-bold'>{`Hook'em Horns!`}</h2>
|
<h2 className='my-4 text-2xl text-ut-burntorange font-bold'>Longhorn Feedback</h2>
|
||||||
<p className='mb-6 text-gray-600'>Your feedback is music to our ears. Thanks for helping us improve!</p>
|
|
||||||
<button
|
|
||||||
className='w-full rounded bg-orange-600 px-4 py-2 text-white font-bold transition duration-300 hover:bg-orange-700'
|
|
||||||
onClick={() => window.close()}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-80 bg-white p-6'>
|
|
||||||
<h2 className='mb-4 text-2xl text-ut-burntorange font-bold'>Longhorn Feedback</h2>
|
|
||||||
<p className='mb-4 text-sm text-ut-black'>Help us make UT Registration Plus even better!</p>
|
<p className='mb-4 text-sm text-ut-black'>Help us make UT Registration Plus even better!</p>
|
||||||
|
|
||||||
<form onSubmit={submitFeedback}>
|
<form onSubmit={submitFeedback}>
|
||||||
<div className='mb-4'>
|
<div className='mb-1'>
|
||||||
<label htmlFor='email' className='mb-1 block text-sm text-ut-black font-medium'>
|
<label htmlFor='email' className='mb-1 block text-sm text-ut-black font-medium'>
|
||||||
Your @utexas.edu email
|
Your @utexas.edu email
|
||||||
</label>
|
</label>
|
||||||
@@ -85,8 +108,13 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
<input
|
<input
|
||||||
type='email'
|
type='email'
|
||||||
id='email'
|
id='email'
|
||||||
value={email}
|
value={emailAddress}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e =>
|
||||||
|
setEmailAddress({
|
||||||
|
emailAddress: e.target.value,
|
||||||
|
rememberMyEmail: rememberMyEmail ?? false,
|
||||||
|
})
|
||||||
|
}
|
||||||
className='w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
|
className='w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
|
||||||
placeholder='bevo@utexas.edu'
|
placeholder='bevo@utexas.edu'
|
||||||
required
|
required
|
||||||
@@ -94,6 +122,23 @@ export default function ReportIssueMain(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-4'>
|
||||||
|
<label className='mb-1 flex cursor-pointer content-center gap-1.25 text-sm text-ut-black font-medium'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='cursor-pointer'
|
||||||
|
checked={rememberMyEmail}
|
||||||
|
onChange={e =>
|
||||||
|
setRememberMyEmail({
|
||||||
|
rememberMyEmail: e.target.checked,
|
||||||
|
emailAddress: emailAddress ?? '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>{' '}
|
||||||
|
Remember my email
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<label htmlFor='feedback' className='mb-1 block text-sm text-ut-black font-medium'>
|
<label htmlFor='feedback' className='mb-1 block text-sm text-ut-black font-medium'>
|
||||||
Your feedback
|
Your feedback
|
||||||
|
|||||||
@@ -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,18 +14,14 @@ 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/',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: "Summer '25 Course Schedule",
|
|
||||||
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 +30,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
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 });
|
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 => ({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user