diff --git a/package.json b/package.json
index 9d8f56fa..2f0750ea 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,9 @@
"@phosphor-icons/react": "^2.1.7",
"@sentry/react": "^8.55.0",
"@tanstack/react-query": "^5.69.0",
+ "@tsparticles/engine": "^3.9.1",
+ "@tsparticles/react": "^3.0.0",
+ "@tsparticles/slim": "^3.9.1",
"@unocss/vite": "^0.63.6",
"@vitejs/plugin-react": "^4.3.4",
"chrome-extension-toolkit": "^0.0.54",
@@ -159,7 +162,10 @@
},
"overrides": {
"es-module-lexer": "^1.5.4"
- }
+ },
+ "onlyBuiltDependencies": [
+ "@tsparticles/engine"
+ ]
},
"volta": {
"node": "20.19.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e794b529..ad5571cb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -49,6 +49,15 @@ importers:
'@tanstack/react-query':
specifier: ^5.69.0
version: 5.69.0(react@18.3.1)
+ '@tsparticles/engine':
+ specifier: ^3.9.1
+ version: 3.9.1
+ '@tsparticles/react':
+ specifier: ^3.0.0
+ version: 3.0.0(@tsparticles/engine@3.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@tsparticles/slim':
+ specifier: ^3.9.1
+ version: 3.9.1
'@unocss/vite':
specifier: ^0.63.6
version: 0.63.6(patch_hash=9e2d2732a6e057a2ca90fba199730f252d8b4db8631b2c6ee0854fce7771bc95)(rollup@4.52.4)(typescript@5.7.3)(vite@5.4.20(@types/node@22.13.5)(sass@1.85.1)(terser@5.44.0))
@@ -2075,6 +2084,121 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
+ '@tsparticles/basic@3.9.1':
+ resolution: {integrity: sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ==}
+
+ '@tsparticles/engine@3.9.1':
+ resolution: {integrity: sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==}
+
+ '@tsparticles/interaction-external-attract@3.9.1':
+ resolution: {integrity: sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg==}
+
+ '@tsparticles/interaction-external-bounce@3.9.1':
+ resolution: {integrity: sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A==}
+
+ '@tsparticles/interaction-external-bubble@3.9.1':
+ resolution: {integrity: sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ==}
+
+ '@tsparticles/interaction-external-connect@3.9.1':
+ resolution: {integrity: sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA==}
+
+ '@tsparticles/interaction-external-grab@3.9.1':
+ resolution: {integrity: sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow==}
+
+ '@tsparticles/interaction-external-pause@3.9.1':
+ resolution: {integrity: sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g==}
+
+ '@tsparticles/interaction-external-push@3.9.1':
+ resolution: {integrity: sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw==}
+
+ '@tsparticles/interaction-external-remove@3.9.1':
+ resolution: {integrity: sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA==}
+
+ '@tsparticles/interaction-external-repulse@3.9.1':
+ resolution: {integrity: sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw==}
+
+ '@tsparticles/interaction-external-slow@3.9.1':
+ resolution: {integrity: sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ==}
+
+ '@tsparticles/interaction-particles-attract@3.9.1':
+ resolution: {integrity: sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ==}
+
+ '@tsparticles/interaction-particles-collisions@3.9.1':
+ resolution: {integrity: sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w==}
+
+ '@tsparticles/interaction-particles-links@3.9.1':
+ resolution: {integrity: sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg==}
+
+ '@tsparticles/move-base@3.9.1':
+ resolution: {integrity: sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ==}
+
+ '@tsparticles/move-parallax@3.9.1':
+ resolution: {integrity: sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA==}
+
+ '@tsparticles/plugin-easing-quad@3.9.1':
+ resolution: {integrity: sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw==}
+
+ '@tsparticles/plugin-hex-color@3.9.1':
+ resolution: {integrity: sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg==}
+
+ '@tsparticles/plugin-hsl-color@3.9.1':
+ resolution: {integrity: sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w==}
+
+ '@tsparticles/plugin-rgb-color@3.9.1':
+ resolution: {integrity: sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ==}
+
+ '@tsparticles/react@3.0.0':
+ resolution: {integrity: sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q==}
+ peerDependencies:
+ '@tsparticles/engine': ^3.0.2
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@tsparticles/shape-circle@3.9.1':
+ resolution: {integrity: sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g==}
+
+ '@tsparticles/shape-emoji@3.9.1':
+ resolution: {integrity: sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ==}
+
+ '@tsparticles/shape-image@3.9.1':
+ resolution: {integrity: sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA==}
+
+ '@tsparticles/shape-line@3.9.1':
+ resolution: {integrity: sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ==}
+
+ '@tsparticles/shape-polygon@3.9.1':
+ resolution: {integrity: sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ==}
+
+ '@tsparticles/shape-square@3.9.1':
+ resolution: {integrity: sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ==}
+
+ '@tsparticles/shape-star@3.9.1':
+ resolution: {integrity: sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA==}
+
+ '@tsparticles/slim@3.9.1':
+ resolution: {integrity: sha512-CL5cDmADU7sDjRli0So+hY61VMbdroqbArmR9Av+c1Fisa5ytr6QD7Jv62iwU2S6rvgicEe9OyRmSy5GIefwZw==}
+
+ '@tsparticles/updater-color@3.9.1':
+ resolution: {integrity: sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg==}
+
+ '@tsparticles/updater-life@3.9.1':
+ resolution: {integrity: sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA==}
+
+ '@tsparticles/updater-opacity@3.9.1':
+ resolution: {integrity: sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw==}
+
+ '@tsparticles/updater-out-modes@3.9.1':
+ resolution: {integrity: sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg==}
+
+ '@tsparticles/updater-rotate@3.9.1':
+ resolution: {integrity: sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ==}
+
+ '@tsparticles/updater-size@3.9.1':
+ resolution: {integrity: sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw==}
+
+ '@tsparticles/updater-stroke-color@3.9.1':
+ resolution: {integrity: sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA==}
+
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -8820,6 +8944,188 @@ snapshots:
'@trysound/sax@0.2.0': {}
+ '@tsparticles/basic@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+ '@tsparticles/move-base': 3.9.1
+ '@tsparticles/plugin-hex-color': 3.9.1
+ '@tsparticles/plugin-hsl-color': 3.9.1
+ '@tsparticles/plugin-rgb-color': 3.9.1
+ '@tsparticles/shape-circle': 3.9.1
+ '@tsparticles/updater-color': 3.9.1
+ '@tsparticles/updater-opacity': 3.9.1
+ '@tsparticles/updater-out-modes': 3.9.1
+ '@tsparticles/updater-size': 3.9.1
+
+ '@tsparticles/engine@3.9.1': {}
+
+ '@tsparticles/interaction-external-attract@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-bounce@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-bubble@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-connect@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-grab@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-pause@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-push@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-remove@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-repulse@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-external-slow@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-particles-attract@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-particles-collisions@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/interaction-particles-links@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/move-base@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/move-parallax@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/plugin-easing-quad@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/plugin-hex-color@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/plugin-hsl-color@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/plugin-rgb-color@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/react@3.0.0(@tsparticles/engine@3.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@tsparticles/shape-circle@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/shape-emoji@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/shape-image@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/shape-line@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/shape-polygon@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/shape-square@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/shape-star@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/slim@3.9.1':
+ dependencies:
+ '@tsparticles/basic': 3.9.1
+ '@tsparticles/engine': 3.9.1
+ '@tsparticles/interaction-external-attract': 3.9.1
+ '@tsparticles/interaction-external-bounce': 3.9.1
+ '@tsparticles/interaction-external-bubble': 3.9.1
+ '@tsparticles/interaction-external-connect': 3.9.1
+ '@tsparticles/interaction-external-grab': 3.9.1
+ '@tsparticles/interaction-external-pause': 3.9.1
+ '@tsparticles/interaction-external-push': 3.9.1
+ '@tsparticles/interaction-external-remove': 3.9.1
+ '@tsparticles/interaction-external-repulse': 3.9.1
+ '@tsparticles/interaction-external-slow': 3.9.1
+ '@tsparticles/interaction-particles-attract': 3.9.1
+ '@tsparticles/interaction-particles-collisions': 3.9.1
+ '@tsparticles/interaction-particles-links': 3.9.1
+ '@tsparticles/move-parallax': 3.9.1
+ '@tsparticles/plugin-easing-quad': 3.9.1
+ '@tsparticles/shape-emoji': 3.9.1
+ '@tsparticles/shape-image': 3.9.1
+ '@tsparticles/shape-line': 3.9.1
+ '@tsparticles/shape-polygon': 3.9.1
+ '@tsparticles/shape-square': 3.9.1
+ '@tsparticles/shape-star': 3.9.1
+ '@tsparticles/updater-life': 3.9.1
+ '@tsparticles/updater-rotate': 3.9.1
+ '@tsparticles/updater-stroke-color': 3.9.1
+
+ '@tsparticles/updater-color@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/updater-life@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/updater-opacity@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/updater-out-modes@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/updater-rotate@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/updater-size@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
+ '@tsparticles/updater-stroke-color@3.9.1':
+ dependencies:
+ '@tsparticles/engine': 3.9.1
+
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
diff --git a/src/views/components/calendar/CalendarSchedules.tsx b/src/views/components/calendar/CalendarSchedules.tsx
index d7e10ab5..d2cb1555 100644
--- a/src/views/components/calendar/CalendarSchedules.tsx
+++ b/src/views/components/calendar/CalendarSchedules.tsx
@@ -27,7 +27,7 @@ export function CalendarSchedules() {
return (
-
+
MY SCHEDULES
diff --git a/src/views/components/settings/AdvancedSettings.tsx b/src/views/components/settings/AdvancedSettings.tsx
new file mode 100644
index 00000000..4b2707fe
--- /dev/null
+++ b/src/views/components/settings/AdvancedSettings.tsx
@@ -0,0 +1,197 @@
+import { Trash } from '@phosphor-icons/react';
+import { OptionsStore } from '@shared/storage/OptionsStore';
+import MIMEType from '@shared/types/MIMEType';
+import type { UserSchedule } from '@shared/types/UserSchedule';
+import { handleExportJson } from '@views/components/calendar/utils';
+import { Button } from '@views/components/common/Button';
+import Divider from '@views/components/common/Divider';
+import SwitchButton from '@views/components/common/SwitchButton';
+import Text from '@views/components/common/Text/Text';
+import clsx from 'clsx';
+import React from 'react';
+
+import FileUpload from '../common/FileUpload';
+import { DISPLAY_PREVIEWS, PREVIEW_SECTION_DIV_CLASSNAME } from './constants';
+import Preview from './Preview';
+
+interface AdvancedSettingsProps {
+ highlightConflicts: boolean;
+ setHighlightConflicts: (value: boolean) => void;
+ loadAllCourses: boolean;
+ setLoadAllCourses: (value: boolean) => void;
+ increaseScheduleLimit: boolean;
+ setIncreaseScheduleLimit: (value: boolean) => void;
+ calendarNewTab: boolean;
+ setCalendarNewTab: (value: boolean) => void;
+ activeSchedule: UserSchedule;
+ handleEraseAll: () => void;
+ handleImportClick: (event: React.ChangeEvent
) => Promise;
+}
+
+/**
+ * Settings section component for advanced settings
+ */
+export const AdvancedSettings: React.FC = ({
+ highlightConflicts,
+ setHighlightConflicts,
+ loadAllCourses,
+ setLoadAllCourses,
+ increaseScheduleLimit,
+ setIncreaseScheduleLimit,
+ calendarNewTab,
+ setCalendarNewTab,
+ activeSchedule,
+ handleEraseAll,
+ handleImportClick,
+}) => (
+
+ ADVANCED SETTINGS
+
+
+
+
+
+ Export Current Schedule
+
+
Backup your active schedule to a portable file
+
+
+
+
+
+
+
+
+
+ Import Schedule
+
+
Import from a schedule file
+
+
+ Import Schedule
+
+
+
+
+
+
+
+
+ Course Conflict Highlight
+
+
+ Adds a red strikethrough to courses that have conflicting times.
+
+
+
{
+ setHighlightConflicts(!highlightConflicts);
+ OptionsStore.set('enableHighlightConflicts', !highlightConflicts);
+ }}
+ />
+
+
+
+
+
+
+
+ Load All Courses in Course Schedule
+
+
+ Loads all courses in the Course Schedule site by scrolling, instead of using next/prev page
+ buttons.
+
+
+
{
+ setLoadAllCourses(!loadAllCourses);
+ OptionsStore.set('enableScrollToLoad', !loadAllCourses);
+ }}
+ />
+
+
+
+
+
+
+
+ Allow more than 10 schedules
+
+
+ Allow bypassing the 10-schedule limit. Intended for advisors or staff who need to create
+ many schedules on behalf of students.
+
+
+
{
+ setIncreaseScheduleLimit(!increaseScheduleLimit);
+ OptionsStore.set('allowMoreSchedules', !increaseScheduleLimit);
+ }}
+ />
+
+
+
+
+
+
+
+ Always Open Calendar in New Tab
+
+
+ Always opens the calendar view in a new tab when navigating to the calendar page. May
+ prevent issues where the calendar refuses to open.
+
+
+
{
+ setCalendarNewTab(!calendarNewTab);
+ OptionsStore.set('alwaysOpenCalendarInNewTab', !calendarNewTab);
+ }}
+ />
+
+
+
+
+
+
+
+ Reset All Data
+
+
Erases all schedules and courses you have.
+
+
+
+
+ {DISPLAY_PREVIEWS && (
+
+
+ 01234 MWF 10:00 AM - 11:00 AM UTC 1.234
+
+
+ )}
+
+
+);
diff --git a/src/views/components/settings/ContributorCard.tsx b/src/views/components/settings/ContributorCard.tsx
new file mode 100644
index 00000000..af7efb61
--- /dev/null
+++ b/src/views/components/settings/ContributorCard.tsx
@@ -0,0 +1,54 @@
+import Text from '@views/components/common/Text/Text';
+import React from 'react';
+
+interface ContributorCardProps {
+ name: string;
+ githubUsername: string;
+ roles: string[];
+ stats?: {
+ commits: number;
+ linesAdded: number;
+ linesDeleted: number;
+ mergedPRs?: number;
+ };
+ showStats: boolean;
+ includeMergedPRs: boolean;
+}
+
+/**
+ * GitHub contributor card component
+ */
+export const ContributorCard: React.FC = ({
+ name,
+ githubUsername,
+ roles,
+ stats,
+ showStats,
+ includeMergedPRs,
+}) => (
+
+
window.open(`https://github.com/${githubUsername}`, '_blank')}
+ >
+ {name}
+
+ {roles.map(role => (
+
+ {role}
+
+ ))}
+ {showStats && stats && (
+
+
GitHub Stats (UTRP repo):
+ {includeMergedPRs && stats.mergedPRs !== undefined && (
+
Merged PRs: {stats.mergedPRs}
+ )}
+
Commits: {stats.commits}
+
{stats.linesAdded}++
+
{stats.linesDeleted}--
+
+ )}
+
+);
diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx
index f6218d3b..daa62765 100644
--- a/src/views/components/settings/Settings.tsx
+++ b/src/views/components/settings/Settings.tsx
@@ -1,115 +1,68 @@
-// import addCourse from '@pages/background/lib/addCourse';
+// Pages
import { addCourseByURL } from '@pages/background/lib/addCourseByURL';
import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule';
import importSchedule from '@pages/background/lib/importSchedule';
-import { CalendarDots, Trash } from '@phosphor-icons/react';
+import { CalendarDots } from '@phosphor-icons/react';
+// Shared
import { background } from '@shared/messages';
import { DevStore } from '@shared/storage/DevStore';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { CRX_PAGES } from '@shared/types/CRXPages';
-import MIMEType from '@shared/types/MIMEType';
-// import { addCourseByUrl } from '@shared/util/courseUtils';
-// import { getCourseColors } from '@shared/util/colors';
-// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
+import Particles from '@tsparticles/react';
import { Button } from '@views/components/common/Button';
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
+// Views
import Divider from '@views/components/common/Divider';
import { LargeLogo } from '@views/components/common/LogoIcon';
-// import PopupCourseBlock from '@views/components/common/PopupCourseBlock';
-import SwitchButton from '@views/components/common/SwitchButton';
import Text from '@views/components/common/Text/Text';
+// Hooks
import useChangelog from '@views/hooks/useChangelog';
import useSchedules from '@views/hooks/useSchedules';
-// import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
-// import getCourseTableRows from '@views/lib/getCourseTableRows';
import { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats';
-// import { SiteSupport } from '@views/lib/getSiteSupport';
-import clsx from 'clsx';
-import React, { useCallback, useEffect, useState } from 'react';
+// Misc
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+// Icons
import IconoirGitFork from '~icons/iconoir/git-fork';
-import { handleExportJson } from '../calendar/utils';
-// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';;
-import FileUpload from '../common/FileUpload';
import { useMigrationDialog } from '../common/MigrationDialog';
-// import RefreshIcon from '~icons/material-symbols/refresh';
+import { AdvancedSettings } from './AdvancedSettings';
+import { DEV_MODE_CLICK_TARGET, INCLUDE_MERGED_PRS, STATS_TOGGLE_KEY } from './constants';
+import { ContributorCard } from './ContributorCard';
import DevMode from './DevMode';
-import Preview from './Preview';
+import { useBirthdayCelebration } from './useBirthdayCelebration';
+import { useDevMode } from './useDevMode';
const manifest = chrome.runtime.getManifest();
-const gitHubStatsService = new GitHubStatsService();
-const includeMergedPRs = false;
-
-const DISPLAY_PREVIEWS = false;
-const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4';
-
/**
- * Custom hook for enabling developer mode.
- *
- * @param targetCount - The target count to activate developer mode.
- * @returns A tuple containing a boolean indicating if developer mode is active and a function to increment the count.
- */
-const useDevMode = (targetCount: number): [boolean, () => void] => {
- const [count, setCount] = useState(0);
- const [active, setActive] = useState(false);
- const [lastClick, setLastClick] = useState(0);
-
- const incrementCount = useCallback(() => {
- const now = Date.now();
- if (now - lastClick < 500) {
- setCount(prevCount => {
- const newCount = prevCount + 1;
- if (newCount === targetCount) {
- setActive(true);
- }
- return newCount;
- });
- } else {
- setCount(1);
- }
- setLastClick(now);
- }, [lastClick, targetCount]);
-
- useEffect(() => {
- const timer = setTimeout(() => setCount(0), 3000);
- return () => clearTimeout(timer);
- }, [count]);
-
- return [active, incrementCount];
-};
-
-/**
- * Component for managing user settings and preferences.
+ * Main Settings Component for managing user settings and preferences.
*
* @returns The Settings component.
*/
export default function Settings(): JSX.Element {
- const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState(false);
- // const [_showTimeLocation, setShowTimeLocation] = useState(false);
- const [highlightConflicts, setHighlightConflicts] = useState(false);
- const [loadAllCourses, setLoadAllCourses] = useState(false);
- const [_enableDataRefreshing, setEnableDataRefreshing] = useState(false);
- const [calendarNewTab, setCalendarNewTab] = useState(false);
- const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState(false);
+ const gitHubStatsService = useMemo(() => new GitHubStatsService(), []);
- const showMigrationDialog = useMigrationDialog();
-
- // Toggle GitHub stats when the user presses the 'S' key
- const [showGitHubStats, setShowGitHubStats] = useState(false);
+ // State
+ const [highlightConflicts, setHighlightConflicts] = useState(false);
+ const [loadAllCourses, setLoadAllCourses] = useState(false);
+ const [calendarNewTab, setCalendarNewTab] = useState(false);
+ const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState(false);
+ const [showGitHubStats, setShowGitHubStats] = useState(false);
const [githubStats, setGitHubStats] = useState
> | null>(null);
+ const [isDeveloper, setIsDeveloper] = useState(false);
const [activeSchedule] = useSchedules();
- // const [isRefreshing, setIsRefreshing] = useState(false);
-
- const [isDeveloper, setIsDeveloper] = useState(false);
-
const showDialog = usePrompt();
const handleChangelogOnClick = useChangelog();
+ const showMigrationDialog = useMigrationDialog();
+ const [devMode, toggleDevMode] = useDevMode(DEV_MODE_CLICK_TARGET);
+ const { showParticles, particlesInit, particlesOptions, triggerCelebration, isBirthday } = useBirthdayCelebration();
+
+ // Initialize settings and listeners
useEffect(() => {
const fetchGitHubStats = async () => {
try {
@@ -121,19 +74,10 @@ export default function Settings(): JSX.Element {
};
const initAndSetSettings = async () => {
- const {
- enableCourseStatusChips,
- enableHighlightConflicts,
- enableScrollToLoad,
- enableDataRefreshing,
- alwaysOpenCalendarInNewTab,
- allowMoreSchedules,
- } = await initSettings();
- setEnableCourseStatusChips(enableCourseStatusChips);
- // setShowTimeLocation(enableTimeAndLocationInPopup);
+ const { enableHighlightConflicts, enableScrollToLoad, alwaysOpenCalendarInNewTab, allowMoreSchedules } =
+ await initSettings();
setHighlightConflicts(enableHighlightConflicts);
setLoadAllCourses(enableScrollToLoad);
- setEnableDataRefreshing(enableDataRefreshing);
setCalendarNewTab(alwaysOpenCalendarInNewTab);
setIncreaseScheduleLimit(allowMoreSchedules);
};
@@ -143,79 +87,50 @@ export default function Settings(): JSX.Element {
setIsDeveloper(isDev);
};
+ const handleKeyPress = (event: KeyboardEvent) => {
+ if (event.key === STATS_TOGGLE_KEY || event.key === STATS_TOGGLE_KEY.toUpperCase()) {
+ setShowGitHubStats(prev => !prev);
+ }
+ };
+
+ // Listeners
const ds_l1 = DevStore.listen('isDeveloper', async ({ newValue }) => {
setIsDeveloper(newValue);
});
+ const l1 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
+ setHighlightConflicts(newValue);
+ });
+
+ const l2 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
+ setLoadAllCourses(newValue);
+ });
+
+ const l3 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
+ setCalendarNewTab(newValue);
+ });
+
+ const l4 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
+ setIncreaseScheduleLimit(newValue);
+ });
+
+ window.addEventListener('keydown', handleKeyPress);
+
initDS();
fetchGitHubStats();
initAndSetSettings();
- const handleKeyPress = (event: KeyboardEvent) => {
- if (event.key === 'S' || event.key === 's') {
- setShowGitHubStats(prev => !prev);
- }
- };
-
- window.addEventListener('keydown', handleKeyPress);
-
- // Listen for changes in the settings
- const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
- setEnableCourseStatusChips(newValue);
- // console.log('enableCourseStatusChips', newValue);
- });
-
- // const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => {
- // setShowTimeLocation(newValue);
- // // console.log('enableTimeAndLocationInPopup', newValue);
- // });
-
- const l2 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
- setHighlightConflicts(newValue);
- // console.log('enableHighlightConflicts', newValue);
- });
-
- const l3 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
- setLoadAllCourses(newValue);
- // console.log('enableScrollToLoad', newValue);
- });
-
- const l4 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
- setEnableDataRefreshing(newValue);
- // console.log('enableDataRefreshing', newValue);
- });
-
- const l5 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
- setCalendarNewTab(newValue);
- // console.log('alwaysOpenCalendarInNewTab', newValue);
- });
-
- const l6 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
- setCalendarNewTab(newValue);
- // console.log('alwaysOpenCalendarInNewTab', newValue);
- });
-
- const l7 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
- setIncreaseScheduleLimit(newValue);
- });
-
- // Remove listeners when the component is unmounted
return () => {
OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2);
OptionsStore.removeListener(l3);
OptionsStore.removeListener(l4);
- OptionsStore.removeListener(l5);
- OptionsStore.removeListener(l6);
- OptionsStore.removeListener(l7);
-
DevStore.removeListener(ds_l1);
-
window.removeEventListener('keydown', handleKeyPress);
};
- }, []);
+ }, [gitHubStatsService]);
- const handleEraseAll = () => {
+ const handleEraseAll = useCallback(() => {
showDialog({
title: 'Erase All Course/Schedule Data',
description: (
@@ -242,9 +157,9 @@ export default function Settings(): JSX.Element {
),
});
- };
+ }, [showDialog]);
- const handleImportClick = async (event: React.ChangeEvent) => {
+ const handleImportClick = useCallback(async (event: React.ChangeEvent) => {
const file = event.target.files?.[0];
if (!file) return;
@@ -257,16 +172,30 @@ export default function Settings(): JSX.Element {
console.error('Error importing schedule:', error);
alert('Failed to import schedule. Make sure the file is a valid .json format.');
}
- };
- // const handleAddCourseByLink = async () => {
- // // todo: Use a proper modal instead of a prompt
- // const link: string | null = prompt('Enter course link');
- // // Exit if the user cancels the prompt
- // if (link === null) return;
- // await addCourseByUrl(link, activeSchedule);
- // };
+ }, []);
- const [devMode, toggleDevMode] = useDevMode(10);
+ const sortedContributors = useMemo(() => {
+ if (!githubStats) return LONGHORN_DEVELOPERS_SWE;
+ return [...LONGHORN_DEVELOPERS_SWE].sort(
+ (a, b) =>
+ (githubStats.userGitHubStats[b.githubUsername]?.commits ?? 0) -
+ (githubStats.userGitHubStats[a.githubUsername]?.commits ?? 0)
+ );
+ }, [githubStats]);
+
+ const additionalContributors = useMemo(() => {
+ if (!githubStats) return [];
+ return Object.keys(githubStats.userGitHubStats)
+ .filter(
+ username =>
+ !LONGHORN_DEVELOPERS_ADMINS.some(admin => admin.githubUsername === username) &&
+ !LONGHORN_DEVELOPERS_SWE.some(swe => swe.githubUsername === username)
+ )
+ .sort(
+ (a, b) =>
+ (githubStats.userGitHubStats[b]?.commits ?? 0) - (githubStats.userGitHubStats[a]?.commits ?? 0)
+ );
+ }, [githubStats]);
if (devMode) {
DevStore.set('isDeveloper', true);
@@ -274,13 +203,32 @@ export default function Settings(): JSX.Element {
}
return (
-
+
+ {particlesInit && showParticles && (
+
+ )}
+