feat: LHD birthday (#717)
* chore: add tsparticles/react * fix: imports and lint issues * fix: imports and format * feat: refactor settings and add LHD birthday celebration * chore: lint and format
This commit is contained in:
@@ -39,6 +39,9 @@
|
|||||||
"@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",
|
||||||
|
"@tsparticles/engine": "^3.9.1",
|
||||||
|
"@tsparticles/react": "^3.0.0",
|
||||||
|
"@tsparticles/slim": "^3.9.1",
|
||||||
"@unocss/vite": "^0.63.6",
|
"@unocss/vite": "^0.63.6",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"chrome-extension-toolkit": "^0.0.54",
|
"chrome-extension-toolkit": "^0.0.54",
|
||||||
@@ -159,7 +162,10 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"es-module-lexer": "^1.5.4"
|
"es-module-lexer": "^1.5.4"
|
||||||
}
|
},
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@tsparticles/engine"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.19.4",
|
"node": "20.19.4",
|
||||||
|
|||||||
306
pnpm-lock.yaml
generated
306
pnpm-lock.yaml
generated
@@ -49,6 +49,15 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.69.0
|
specifier: ^5.69.0
|
||||||
version: 5.69.0(react@18.3.1)
|
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':
|
'@unocss/vite':
|
||||||
specifier: ^0.63.6
|
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))
|
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==}
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
engines: {node: '>=10.13.0'}
|
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':
|
'@types/aria-query@5.0.4':
|
||||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||||
|
|
||||||
@@ -8820,6 +8944,188 @@ snapshots:
|
|||||||
|
|
||||||
'@trysound/sax@0.2.0': {}
|
'@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/aria-query@5.0.4': {}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function CalendarSchedules() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-w-full w-0 flex flex-col items-center gap-y-spacing-2'>
|
<div className='min-w-full w-0 flex flex-col items-center gap-y-spacing-2'>
|
||||||
<div className='m0 w-full flex justify-between items-center'>
|
<div className='m0 w-full flex items-center justify-between'>
|
||||||
<Text variant='h3' className='text-nowrap text-theme-black'>
|
<Text variant='h3' className='text-nowrap text-theme-black'>
|
||||||
MY SCHEDULES
|
MY SCHEDULES
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
197
src/views/components/settings/AdvancedSettings.tsx
Normal file
197
src/views/components/settings/AdvancedSettings.tsx
Normal file
@@ -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<HTMLInputElement>) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings section component for advanced settings
|
||||||
|
*/
|
||||||
|
export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
||||||
|
highlightConflicts,
|
||||||
|
setHighlightConflicts,
|
||||||
|
loadAllCourses,
|
||||||
|
setLoadAllCourses,
|
||||||
|
increaseScheduleLimit,
|
||||||
|
setIncreaseScheduleLimit,
|
||||||
|
calendarNewTab,
|
||||||
|
setCalendarNewTab,
|
||||||
|
activeSchedule,
|
||||||
|
handleEraseAll,
|
||||||
|
handleImportClick,
|
||||||
|
}) => (
|
||||||
|
<section className='mb-8'>
|
||||||
|
<h2 className='mb-4 text-xl text-ut-black font-semibold'>ADVANCED SETTINGS</h2>
|
||||||
|
<div className='flex space-x-4'>
|
||||||
|
<div className={PREVIEW_SECTION_DIV_CLASSNAME}>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Export Current Schedule
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>Backup your active schedule to a portable file</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='ut-burntorange'
|
||||||
|
onClick={() => handleExportJson(activeSchedule.id)}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Import Schedule
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>Import from a schedule file</p>
|
||||||
|
</div>
|
||||||
|
<FileUpload
|
||||||
|
variant='filled'
|
||||||
|
color='ut-burntorange'
|
||||||
|
onChange={handleImportClick}
|
||||||
|
accept={MIMEType.JSON}
|
||||||
|
>
|
||||||
|
Import Schedule
|
||||||
|
</FileUpload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Course Conflict Highlight
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
Adds a red strikethrough to courses that have conflicting times.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SwitchButton
|
||||||
|
isChecked={highlightConflicts}
|
||||||
|
onChange={() => {
|
||||||
|
setHighlightConflicts(!highlightConflicts);
|
||||||
|
OptionsStore.set('enableHighlightConflicts', !highlightConflicts);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Load All Courses in Course Schedule
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
Loads all courses in the Course Schedule site by scrolling, instead of using next/prev page
|
||||||
|
buttons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SwitchButton
|
||||||
|
isChecked={loadAllCourses}
|
||||||
|
onChange={() => {
|
||||||
|
setLoadAllCourses(!loadAllCourses);
|
||||||
|
OptionsStore.set('enableScrollToLoad', !loadAllCourses);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Allow more than 10 schedules
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
Allow bypassing the 10-schedule limit. Intended for advisors or staff who need to create
|
||||||
|
many schedules on behalf of students.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SwitchButton
|
||||||
|
isChecked={increaseScheduleLimit}
|
||||||
|
onChange={() => {
|
||||||
|
setIncreaseScheduleLimit(!increaseScheduleLimit);
|
||||||
|
OptionsStore.set('allowMoreSchedules', !increaseScheduleLimit);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Always Open Calendar in New Tab
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
Always opens the calendar view in a new tab when navigating to the calendar page. May
|
||||||
|
prevent issues where the calendar refuses to open.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SwitchButton
|
||||||
|
isChecked={calendarNewTab}
|
||||||
|
onChange={() => {
|
||||||
|
setCalendarNewTab(!calendarNewTab);
|
||||||
|
OptionsStore.set('alwaysOpenCalendarInNewTab', !calendarNewTab);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='max-w-xs'>
|
||||||
|
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
||||||
|
Reset All Data
|
||||||
|
</Text>
|
||||||
|
<p className='text-sm text-gray-600'>Erases all schedules and courses you have.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant='outline' color='theme-red' icon={Trash} onClick={handleEraseAll}>
|
||||||
|
Erase All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{DISPLAY_PREVIEWS && (
|
||||||
|
<Preview>
|
||||||
|
<Text
|
||||||
|
variant='h2-course'
|
||||||
|
className={clsx('text-center text-theme-red font-normal', {
|
||||||
|
'line-through': highlightConflicts,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
01234 MWF 10:00 AM - 11:00 AM UTC 1.234
|
||||||
|
</Text>
|
||||||
|
</Preview>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
54
src/views/components/settings/ContributorCard.tsx
Normal file
54
src/views/components/settings/ContributorCard.tsx
Normal file
@@ -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<ContributorCardProps> = ({
|
||||||
|
name,
|
||||||
|
githubUsername,
|
||||||
|
roles,
|
||||||
|
stats,
|
||||||
|
showStats,
|
||||||
|
includeMergedPRs,
|
||||||
|
}) => (
|
||||||
|
<div className='border border-gray-300 rounded bg-ut-gray/10 p-4'>
|
||||||
|
<Text
|
||||||
|
variant='p'
|
||||||
|
className='text-ut-burntorange font-semibold hover:cursor-pointer'
|
||||||
|
onClick={() => window.open(`https://github.com/${githubUsername}`, '_blank')}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{roles.map(role => (
|
||||||
|
<p key={`${githubUsername}-${role}`} className='text-sm text-gray-600'>
|
||||||
|
{role}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
{showStats && stats && (
|
||||||
|
<div className='mt-2'>
|
||||||
|
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
|
||||||
|
{includeMergedPRs && stats.mergedPRs !== undefined && (
|
||||||
|
<p className='text-xs'>Merged PRs: {stats.mergedPRs}</p>
|
||||||
|
)}
|
||||||
|
<p className='text-xs'>Commits: {stats.commits}</p>
|
||||||
|
<p className='text-xs text-ut-green'>{stats.linesAdded}++</p>
|
||||||
|
<p className='text-xs text-theme-red'>{stats.linesDeleted}--</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1,115 +1,68 @@
|
|||||||
// import addCourse from '@pages/background/lib/addCourse';
|
// Pages
|
||||||
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 importSchedule from '@pages/background/lib/importSchedule';
|
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 { 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 { CRX_PAGES } from '@shared/types/CRXPages';
|
import { CRX_PAGES } from '@shared/types/CRXPages';
|
||||||
import MIMEType from '@shared/types/MIMEType';
|
import Particles from '@tsparticles/react';
|
||||||
// import { addCourseByUrl } from '@shared/util/courseUtils';
|
|
||||||
// import { getCourseColors } from '@shared/util/colors';
|
|
||||||
// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
|
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
|
||||||
|
// Views
|
||||||
import Divider from '@views/components/common/Divider';
|
import Divider from '@views/components/common/Divider';
|
||||||
import { LargeLogo } from '@views/components/common/LogoIcon';
|
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';
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
// Hooks
|
||||||
import useChangelog from '@views/hooks/useChangelog';
|
import useChangelog from '@views/hooks/useChangelog';
|
||||||
import useSchedules from '@views/hooks/useSchedules';
|
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 { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats';
|
||||||
// import { SiteSupport } from '@views/lib/getSiteSupport';
|
// Misc
|
||||||
import clsx from 'clsx';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
|
// Icons
|
||||||
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 FileUpload from '../common/FileUpload';
|
|
||||||
import { useMigrationDialog } from '../common/MigrationDialog';
|
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 DevMode from './DevMode';
|
||||||
import Preview from './Preview';
|
import { useBirthdayCelebration } from './useBirthdayCelebration';
|
||||||
|
import { useDevMode } from './useDevMode';
|
||||||
|
|
||||||
const manifest = chrome.runtime.getManifest();
|
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.
|
* Main Settings Component for managing user settings and preferences.
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*
|
*
|
||||||
* @returns The Settings component.
|
* @returns The Settings component.
|
||||||
*/
|
*/
|
||||||
export default function Settings(): JSX.Element {
|
export default function Settings(): JSX.Element {
|
||||||
const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
const gitHubStatsService = useMemo(() => new GitHubStatsService(), []);
|
||||||
// const [_showTimeLocation, setShowTimeLocation] = useState<boolean>(false);
|
|
||||||
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(false);
|
|
||||||
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
|
|
||||||
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
|
||||||
const [calendarNewTab, setCalendarNewTab] = useState<boolean>(false);
|
|
||||||
const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const showMigrationDialog = useMigrationDialog();
|
// State
|
||||||
|
const [highlightConflicts, setHighlightConflicts] = useState(false);
|
||||||
// Toggle GitHub stats when the user presses the 'S' key
|
const [loadAllCourses, setLoadAllCourses] = useState(false);
|
||||||
const [showGitHubStats, setShowGitHubStats] = useState<boolean>(false);
|
const [calendarNewTab, setCalendarNewTab] = useState(false);
|
||||||
|
const [increaseScheduleLimit, setIncreaseScheduleLimit] = useState(false);
|
||||||
|
const [showGitHubStats, setShowGitHubStats] = useState(false);
|
||||||
const [githubStats, setGitHubStats] = useState<Awaited<
|
const [githubStats, setGitHubStats] = useState<Awaited<
|
||||||
ReturnType<typeof gitHubStatsService.fetchGitHubStats>
|
ReturnType<typeof gitHubStatsService.fetchGitHubStats>
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
|
const [isDeveloper, setIsDeveloper] = useState(false);
|
||||||
|
|
||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
// const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [isDeveloper, setIsDeveloper] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const showDialog = usePrompt();
|
const showDialog = usePrompt();
|
||||||
const handleChangelogOnClick = useChangelog();
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchGitHubStats = async () => {
|
const fetchGitHubStats = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -121,19 +74,10 @@ export default function Settings(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initAndSetSettings = async () => {
|
const initAndSetSettings = async () => {
|
||||||
const {
|
const { enableHighlightConflicts, enableScrollToLoad, alwaysOpenCalendarInNewTab, allowMoreSchedules } =
|
||||||
enableCourseStatusChips,
|
await initSettings();
|
||||||
enableHighlightConflicts,
|
|
||||||
enableScrollToLoad,
|
|
||||||
enableDataRefreshing,
|
|
||||||
alwaysOpenCalendarInNewTab,
|
|
||||||
allowMoreSchedules,
|
|
||||||
} = await initSettings();
|
|
||||||
setEnableCourseStatusChips(enableCourseStatusChips);
|
|
||||||
// setShowTimeLocation(enableTimeAndLocationInPopup);
|
|
||||||
setHighlightConflicts(enableHighlightConflicts);
|
setHighlightConflicts(enableHighlightConflicts);
|
||||||
setLoadAllCourses(enableScrollToLoad);
|
setLoadAllCourses(enableScrollToLoad);
|
||||||
setEnableDataRefreshing(enableDataRefreshing);
|
|
||||||
setCalendarNewTab(alwaysOpenCalendarInNewTab);
|
setCalendarNewTab(alwaysOpenCalendarInNewTab);
|
||||||
setIncreaseScheduleLimit(allowMoreSchedules);
|
setIncreaseScheduleLimit(allowMoreSchedules);
|
||||||
};
|
};
|
||||||
@@ -143,79 +87,50 @@ export default function Settings(): JSX.Element {
|
|||||||
setIsDeveloper(isDev);
|
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 }) => {
|
const ds_l1 = DevStore.listen('isDeveloper', async ({ newValue }) => {
|
||||||
setIsDeveloper(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();
|
initDS();
|
||||||
fetchGitHubStats();
|
fetchGitHubStats();
|
||||||
initAndSetSettings();
|
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 () => {
|
return () => {
|
||||||
OptionsStore.removeListener(l1);
|
OptionsStore.removeListener(l1);
|
||||||
OptionsStore.removeListener(l2);
|
OptionsStore.removeListener(l2);
|
||||||
OptionsStore.removeListener(l3);
|
OptionsStore.removeListener(l3);
|
||||||
OptionsStore.removeListener(l4);
|
OptionsStore.removeListener(l4);
|
||||||
OptionsStore.removeListener(l5);
|
|
||||||
OptionsStore.removeListener(l6);
|
|
||||||
OptionsStore.removeListener(l7);
|
|
||||||
|
|
||||||
DevStore.removeListener(ds_l1);
|
DevStore.removeListener(ds_l1);
|
||||||
|
|
||||||
window.removeEventListener('keydown', handleKeyPress);
|
window.removeEventListener('keydown', handleKeyPress);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [gitHubStatsService]);
|
||||||
|
|
||||||
const handleEraseAll = () => {
|
const handleEraseAll = useCallback(() => {
|
||||||
showDialog({
|
showDialog({
|
||||||
title: 'Erase All Course/Schedule Data',
|
title: 'Erase All Course/Schedule Data',
|
||||||
description: (
|
description: (
|
||||||
@@ -242,9 +157,9 @@ export default function Settings(): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
};
|
}, [showDialog]);
|
||||||
|
|
||||||
const handleImportClick = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImportClick = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
@@ -257,16 +172,30 @@ export default function Settings(): JSX.Element {
|
|||||||
console.error('Error importing schedule:', error);
|
console.error('Error importing schedule:', error);
|
||||||
alert('Failed to import schedule. Make sure the file is a valid .json format.');
|
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) {
|
if (devMode) {
|
||||||
DevStore.set('isDeveloper', true);
|
DevStore.set('isDeveloper', true);
|
||||||
@@ -274,13 +203,32 @@ export default function Settings(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='relative'>
|
||||||
|
{particlesInit && showParticles && (
|
||||||
|
<Particles
|
||||||
|
id='birthday-particles'
|
||||||
|
options={particlesOptions}
|
||||||
|
className='pointer-events-none absolute inset-0 z-50'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<header className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
|
<header className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
|
||||||
<LargeLogo />
|
<LargeLogo />
|
||||||
<Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' />
|
<Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' />
|
||||||
<Text variant='h1' className='flex-1 text-ut-burntorange normal-case!'>
|
<div className='flex flex-1 items-center gap-2'>
|
||||||
|
<Text variant='h1' className='text-ut-burntorange normal-case'>
|
||||||
Settings and Credits
|
Settings and Credits
|
||||||
</Text>
|
</Text>
|
||||||
|
{isBirthday && (
|
||||||
|
<span
|
||||||
|
onClick={triggerCelebration}
|
||||||
|
className='cursor-pointer px-4 text-sm text-ut-burntorange transition-transform hover:scale-110'
|
||||||
|
title='Click to celebrate!'
|
||||||
|
>
|
||||||
|
🎉 Happy Birthday LHD! 🎉
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'>
|
<div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'>
|
||||||
<Button variant='minimal' color='theme-black' onClick={handleChangelogOnClick}>
|
<Button variant='minimal' color='theme-black' onClick={handleChangelogOnClick}>
|
||||||
<IconoirGitFork className='h-6 w-6 text-ut-gray' />
|
<IconoirGitFork className='h-6 w-6 text-ut-gray' />
|
||||||
@@ -301,238 +249,19 @@ export default function Settings(): JSX.Element {
|
|||||||
|
|
||||||
<div className='p-6 lg:flex'>
|
<div className='p-6 lg:flex'>
|
||||||
<div className='mr-4 lg:w-1/2 xl:w-xl'>
|
<div className='mr-4 lg:w-1/2 xl:w-xl'>
|
||||||
{/* <section className='mb-8'>
|
<AdvancedSettings
|
||||||
<h2 className='mb-4 text-xl text-ut-black font-semibold'>CUSTOMIZATION OPTIONS</h2>
|
highlightConflicts={highlightConflicts}
|
||||||
<div className='flex space-x-4'>
|
setHighlightConflicts={setHighlightConflicts}
|
||||||
<div className='w-1/2 space-y-4'>
|
loadAllCourses={loadAllCourses}
|
||||||
<div className='flex items-center justify-between'>
|
setLoadAllCourses={setLoadAllCourses}
|
||||||
<div className='max-w-xs'>
|
increaseScheduleLimit={increaseScheduleLimit}
|
||||||
<h3 className='text-ut-burntorange font-semibold'>Show Course Status</h3>
|
setIncreaseScheduleLimit={setIncreaseScheduleLimit}
|
||||||
<p className='text-sm text-gray-600'>
|
calendarNewTab={calendarNewTab}
|
||||||
Shows an indicator for waitlisted, cancelled, and closed courses.
|
setCalendarNewTab={setCalendarNewTab}
|
||||||
</p>
|
activeSchedule={activeSchedule}
|
||||||
</div>
|
handleEraseAll={handleEraseAll}
|
||||||
<SwitchButton
|
handleImportClick={handleImportClick}
|
||||||
isChecked={enableCourseStatusChips}
|
|
||||||
onChange={() => {
|
|
||||||
setEnableCourseStatusChips(!enableCourseStatusChips);
|
|
||||||
OptionsStore.set('enableCourseStatusChips', !enableCourseStatusChips);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<h3 className='text-ut-burntorange font-semibold'>
|
|
||||||
Show Time & Location in Popup
|
|
||||||
</h3>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Shows the course's time and location in the extension's popup.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SwitchButton
|
|
||||||
isChecked={showTimeLocation}
|
|
||||||
onChange={() => {
|
|
||||||
setShowTimeLocation(!showTimeLocation);
|
|
||||||
OptionsStore.set('enableTimeAndLocationInPopup', !showTimeLocation);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{DISPLAY_PREVIEWS && (
|
|
||||||
<Preview>
|
|
||||||
<CalendarCourseCell
|
|
||||||
colors={getCourseColors('orange')}
|
|
||||||
courseDeptAndInstr={ExampleCourse.department}
|
|
||||||
className={ExampleCourse.number}
|
|
||||||
status={ExampleCourse.status}
|
|
||||||
timeAndLocation={ExampleCourse.schedule.meetings[0]!.getTimeString({
|
|
||||||
separator: '-',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<PopupCourseBlock colors={getCourseColors('orange')} course={ExampleCourse} />
|
|
||||||
</Preview>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' /> */}
|
|
||||||
|
|
||||||
<section className='mb-8'>
|
|
||||||
<h2 className='mb-4 text-xl text-ut-black font-semibold'>ADVANCED SETTINGS</h2>
|
|
||||||
<div className='flex space-x-4'>
|
|
||||||
<div className={PREVIEW_SECTION_DIV_CLASSNAME}>
|
|
||||||
{/* <div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<h3 className='text-ut-burntorange font-semibold'>Refresh Data</h3>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Refreshes waitlist, course status, and other info with the latest data from
|
|
||||||
UT's site.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='ut-black'
|
|
||||||
icon={RefreshIcon}
|
|
||||||
onClick={() => console.log('Refresh clicked')}
|
|
||||||
disabled={!enableDataRefreshing}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' /> */}
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Export Current Schedule
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Backup your active schedule to a portable file
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='ut-burntorange'
|
|
||||||
onClick={() => handleExportJson(activeSchedule.id)}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Import Schedule
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>Import from a schedule file</p>
|
|
||||||
</div>
|
|
||||||
<FileUpload
|
|
||||||
variant='filled'
|
|
||||||
color='ut-burntorange'
|
|
||||||
onChange={handleImportClick}
|
|
||||||
accept={MIMEType.JSON}
|
|
||||||
>
|
|
||||||
Import Schedule
|
|
||||||
</FileUpload>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Course Conflict Highlight
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Adds a red strikethrough to courses that have conflicting times.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SwitchButton
|
|
||||||
isChecked={highlightConflicts}
|
|
||||||
onChange={() => {
|
|
||||||
setHighlightConflicts(!highlightConflicts);
|
|
||||||
OptionsStore.set('enableHighlightConflicts', !highlightConflicts);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Load All Courses in Course Schedule
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Loads all courses in the Course Schedule site by scrolling, instead of using
|
|
||||||
next/prev page buttons.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SwitchButton
|
|
||||||
isChecked={loadAllCourses}
|
|
||||||
onChange={() => {
|
|
||||||
setLoadAllCourses(!loadAllCourses);
|
|
||||||
OptionsStore.set('enableScrollToLoad', !loadAllCourses);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Allow more than 10 schedules
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Allow bypassing the 10-schedule limit. Intended for advisors or staff who
|
|
||||||
need to create many schedules on behalf of students.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SwitchButton
|
|
||||||
isChecked={increaseScheduleLimit}
|
|
||||||
onChange={() => {
|
|
||||||
setIncreaseScheduleLimit(!increaseScheduleLimit);
|
|
||||||
OptionsStore.set('allowMoreSchedules', !increaseScheduleLimit);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Always Open Calendar in New Tab
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Always opens the calendar view in a new tab when navigating to the calendar
|
|
||||||
page. May prevent issues where the calendar refuses to open.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SwitchButton
|
|
||||||
isChecked={calendarNewTab}
|
|
||||||
onChange={() => {
|
|
||||||
setCalendarNewTab(!calendarNewTab);
|
|
||||||
OptionsStore.set('alwaysOpenCalendarInNewTab', !calendarNewTab);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='max-w-xs'>
|
|
||||||
<Text variant='h4' className='text-ut-burntorange font-semibold'>
|
|
||||||
Reset All Data
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
Erases all schedules and courses you have.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button variant='outline' color='theme-red' icon={Trash} onClick={handleEraseAll}>
|
|
||||||
Erase All
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{DISPLAY_PREVIEWS && (
|
|
||||||
<Preview>
|
|
||||||
<Text
|
|
||||||
variant='h2-course'
|
|
||||||
className={clsx('text-center text-theme-red !font-normal', {
|
|
||||||
'line-through': highlightConflicts,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
01234 MWF 10:00 AM - 11:00 AM UTC 1.234
|
|
||||||
</Text>
|
|
||||||
</Preview>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
@@ -593,17 +322,21 @@ export default function Settings(): JSX.Element {
|
|||||||
Open Debug Page
|
Open Debug Page
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider size='auto' orientation='horizontal' />
|
<Divider size='auto' orientation='horizontal' />
|
||||||
|
|
||||||
<Button variant='filled' color='ut-black' onClick={() => addCourseByURL(activeSchedule)}>
|
<Button
|
||||||
|
variant='filled'
|
||||||
|
color='ut-black'
|
||||||
|
onClick={() => addCourseByURL(activeSchedule)}
|
||||||
|
>
|
||||||
Add course by link
|
Add course by link
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='filled' color='ut-burntorange' onClick={showMigrationDialog}>
|
<Button variant='filled' color='ut-burntorange' onClick={showMigrationDialog}>
|
||||||
Show Migration Dialog
|
Show Migration Dialog
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -616,143 +349,43 @@ export default function Settings(): JSX.Element {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'>
|
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'>
|
||||||
{LONGHORN_DEVELOPERS_ADMINS.map(admin => (
|
{LONGHORN_DEVELOPERS_ADMINS.map(admin => (
|
||||||
<div
|
<ContributorCard
|
||||||
key={admin.githubUsername}
|
key={admin.githubUsername}
|
||||||
className='border border-gray-300 rounded bg-ut-gray/10 p-4'
|
name={admin.name}
|
||||||
>
|
githubUsername={admin.githubUsername}
|
||||||
<Text
|
roles={admin.role}
|
||||||
variant='p'
|
stats={githubStats?.adminGitHubStats[admin.githubUsername]}
|
||||||
className='text-ut-burntorange font-semibold hover:cursor-pointer'
|
showStats={showGitHubStats}
|
||||||
onClick={() =>
|
includeMergedPRs={INCLUDE_MERGED_PRS}
|
||||||
window.open(`https://github.com/${admin.githubUsername}`, '_blank')
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
{admin.name}
|
|
||||||
</Text>
|
|
||||||
{admin.role.map(role => (
|
|
||||||
<p key={admin.githubUsername} className='text-sm text-gray-600'>
|
|
||||||
{role}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
{showGitHubStats && githubStats && (
|
|
||||||
<div className='mt-2'>
|
|
||||||
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
|
|
||||||
{includeMergedPRs && (
|
|
||||||
<p className='text-xs'>
|
|
||||||
Merged PRS:{' '}
|
|
||||||
{githubStats.adminGitHubStats[admin.githubUsername]?.mergedPRs}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className='text-xs'>
|
|
||||||
Commits: {githubStats.adminGitHubStats[admin.githubUsername]?.commits}
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-ut-green'>
|
|
||||||
{githubStats.adminGitHubStats[admin.githubUsername]?.linesAdded} ++
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-theme-red'>
|
|
||||||
{githubStats.adminGitHubStats[admin.githubUsername]?.linesDeleted} --
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='my-8'>
|
<section className='my-8'>
|
||||||
<h2 className='mb-4 text-xl text-ut-black font-semibold'>UTRP CONTRIBUTORS</h2>
|
<h2 className='mb-4 text-xl text-ut-black font-semibold'>UTRP CONTRIBUTORS</h2>
|
||||||
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3 xl:grid-cols-3'>
|
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3 xl:grid-cols-3'>
|
||||||
{LONGHORN_DEVELOPERS_SWE.sort(
|
{sortedContributors.map(swe => (
|
||||||
(a, b) =>
|
<ContributorCard
|
||||||
(githubStats?.userGitHubStats[b.githubUsername]?.commits ?? 0) -
|
|
||||||
(githubStats?.userGitHubStats[a.githubUsername]?.commits ?? 0)
|
|
||||||
).map(swe => (
|
|
||||||
<div
|
|
||||||
key={swe.githubUsername}
|
key={swe.githubUsername}
|
||||||
className='border border-gray-300 rounded bg-ut-gray/10 p-4'
|
name={swe.name}
|
||||||
>
|
githubUsername={swe.githubUsername}
|
||||||
<Text
|
roles={swe.role}
|
||||||
variant='p'
|
stats={githubStats?.userGitHubStats[swe.githubUsername]}
|
||||||
className='text-ut-burntorange font-semibold hover:cursor-pointer'
|
showStats={showGitHubStats}
|
||||||
onClick={() =>
|
includeMergedPRs={INCLUDE_MERGED_PRS}
|
||||||
window.open(`https://github.com/${swe.githubUsername}`, '_blank')
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
{swe.name}
|
|
||||||
</Text>
|
|
||||||
{swe.role.map(role => (
|
|
||||||
<p key={swe.githubUsername} className='text-sm text-gray-600'>
|
|
||||||
{role}
|
|
||||||
</p>
|
|
||||||
))}
|
))}
|
||||||
{showGitHubStats && githubStats && (
|
{additionalContributors.map(username => (
|
||||||
<div className='mt-2'>
|
<ContributorCard
|
||||||
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
|
|
||||||
{includeMergedPRs && (
|
|
||||||
<p className='text-xs'>
|
|
||||||
Merged PRS:{' '}
|
|
||||||
{githubStats.userGitHubStats[swe.githubUsername]?.mergedPRs}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className='text-xs'>
|
|
||||||
Commits: {githubStats.userGitHubStats[swe.githubUsername]?.commits}
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-ut-green'>
|
|
||||||
{githubStats.userGitHubStats[swe.githubUsername]?.linesAdded} ++
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-theme-red'>
|
|
||||||
{githubStats.userGitHubStats[swe.githubUsername]?.linesDeleted} --
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{githubStats &&
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
.map(username => (
|
|
||||||
<div
|
|
||||||
key={username}
|
key={username}
|
||||||
className='overflow-clip border border-gray-300 rounded bg-ut-gray/10 p-4'
|
name={githubStats!.names[username] || username}
|
||||||
>
|
githubUsername={username}
|
||||||
<Text
|
roles={['Contributor']}
|
||||||
variant='p'
|
stats={githubStats!.userGitHubStats[username]}
|
||||||
className='text-ut-burntorange font-semibold hover:cursor-pointer'
|
showStats={showGitHubStats}
|
||||||
onClick={() => window.open(`https://github.com/${username}`, '_blank')}
|
includeMergedPRs={INCLUDE_MERGED_PRS}
|
||||||
>
|
/>
|
||||||
{githubStats.names[username]}
|
|
||||||
</Text>
|
|
||||||
<p className='text-sm text-gray-600'>Contributor</p>
|
|
||||||
{showGitHubStats && (
|
|
||||||
<div className='mt-2'>
|
|
||||||
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
|
|
||||||
{includeMergedPRs && (
|
|
||||||
<p className='text-xs'>
|
|
||||||
Merged PRs:{' '}
|
|
||||||
{githubStats.userGitHubStats[username]?.mergedPRs}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className='text-xs'>
|
|
||||||
Commits: {githubStats.userGitHubStats[username]?.commits}
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-ut-green'>
|
|
||||||
{githubStats.userGitHubStats[username]?.linesAdded} ++
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-theme-red'>
|
|
||||||
{githubStats.userGitHubStats[username]?.linesDeleted} --
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
13
src/views/components/settings/constants.ts
Normal file
13
src/views/components/settings/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const DISPLAY_PREVIEWS = false;
|
||||||
|
export const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4';
|
||||||
|
|
||||||
|
export const STATS_TOGGLE_KEY = 's';
|
||||||
|
export const INCLUDE_MERGED_PRS = false;
|
||||||
|
export const DEV_MODE_CLICK_TARGET = 5;
|
||||||
|
export const DEV_MODE_CLICK_TIMEOUT = 5000;
|
||||||
|
export const DEV_MODE_CLICK_INTERVAL = 500;
|
||||||
|
|
||||||
|
// LHD Birthday: January 9th, 2025
|
||||||
|
export const LHD_BIRTHDAY = { month: 0, day: 9 };
|
||||||
|
export const BIRTHDAY_CELEBRATION_DURATION = 5000;
|
||||||
|
export const BIRTHDAY_CELEBRATION_DEBOUNCE = 2000;
|
||||||
140
src/views/components/settings/useBirthdayCelebration.ts
Normal file
140
src/views/components/settings/useBirthdayCelebration.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type { Engine, ISourceOptions } from '@tsparticles/engine';
|
||||||
|
import { initParticlesEngine } from '@tsparticles/react';
|
||||||
|
import { loadSlim } from '@tsparticles/slim';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { BIRTHDAY_CELEBRATION_DEBOUNCE, BIRTHDAY_CELEBRATION_DURATION, LHD_BIRTHDAY } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for birthday celebration particles
|
||||||
|
*/
|
||||||
|
export const useBirthdayCelebration = () => {
|
||||||
|
const [showParticles, setShowParticles] = useState(false);
|
||||||
|
const [particlesInit, setParticlesInit] = useState(false);
|
||||||
|
const [lastCelebration, setLastCelebration] = useState(0);
|
||||||
|
|
||||||
|
const isBirthday = useMemo(() => {
|
||||||
|
const today = new Date();
|
||||||
|
return today.getMonth() === LHD_BIRTHDAY.month && today.getDate() === LHD_BIRTHDAY.day;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initParticlesEngine(async (engine: Engine) => {
|
||||||
|
await loadSlim(engine);
|
||||||
|
}).then(() => {
|
||||||
|
setParticlesInit(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const triggerCelebration = useCallback(() => {
|
||||||
|
if (!isBirthday) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
// Debounce: prevent triggering again within BIRTHDAY_CELEBRATION_DEBOUNCE ms
|
||||||
|
if (now - lastCelebration < BIRTHDAY_CELEBRATION_DEBOUNCE) return;
|
||||||
|
|
||||||
|
setLastCelebration(now);
|
||||||
|
setShowParticles(true);
|
||||||
|
setTimeout(() => setShowParticles(false), BIRTHDAY_CELEBRATION_DURATION);
|
||||||
|
}, [isBirthday, lastCelebration]);
|
||||||
|
|
||||||
|
const particlesOptions: ISourceOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
fullScreen: { enable: true, zIndex: 1 },
|
||||||
|
particles: {
|
||||||
|
color: { value: ['#BF5700', '#333F48', '#FFFFFF'] }, // UT colors
|
||||||
|
move: {
|
||||||
|
direction: 'bottom',
|
||||||
|
enable: true,
|
||||||
|
outModes: {
|
||||||
|
default: 'out',
|
||||||
|
},
|
||||||
|
size: true,
|
||||||
|
speed: {
|
||||||
|
min: 1,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
value: 500,
|
||||||
|
density: {
|
||||||
|
enable: true,
|
||||||
|
area: 800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
value: 1,
|
||||||
|
animation: {
|
||||||
|
enable: false,
|
||||||
|
startValue: 'max',
|
||||||
|
destroy: 'min',
|
||||||
|
speed: 0.3,
|
||||||
|
sync: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
value: {
|
||||||
|
min: 0,
|
||||||
|
max: 360,
|
||||||
|
},
|
||||||
|
direction: 'random',
|
||||||
|
move: true,
|
||||||
|
animation: {
|
||||||
|
enable: true,
|
||||||
|
speed: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tilt: {
|
||||||
|
direction: 'random',
|
||||||
|
enable: true,
|
||||||
|
move: true,
|
||||||
|
value: {
|
||||||
|
min: 0,
|
||||||
|
max: 360,
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
enable: true,
|
||||||
|
speed: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
type: ['circle', 'square'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
value: {
|
||||||
|
min: 2,
|
||||||
|
max: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roll: {
|
||||||
|
darken: {
|
||||||
|
enable: true,
|
||||||
|
value: 30,
|
||||||
|
},
|
||||||
|
enlighten: {
|
||||||
|
enable: true,
|
||||||
|
value: 30,
|
||||||
|
},
|
||||||
|
enable: true,
|
||||||
|
speed: {
|
||||||
|
min: 15,
|
||||||
|
max: 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wobble: {
|
||||||
|
distance: 30,
|
||||||
|
enable: true,
|
||||||
|
move: true,
|
||||||
|
speed: {
|
||||||
|
min: -15,
|
||||||
|
max: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { showParticles, particlesInit, particlesOptions, triggerCelebration, isBirthday };
|
||||||
|
};
|
||||||
35
src/views/components/settings/useDevMode.ts
Normal file
35
src/views/components/settings/useDevMode.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { DEV_MODE_CLICK_INTERVAL, DEV_MODE_CLICK_TIMEOUT } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for enabling developer mode via rapid clicking
|
||||||
|
*/
|
||||||
|
export 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 < DEV_MODE_CLICK_INTERVAL) {
|
||||||
|
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), DEV_MODE_CLICK_TIMEOUT);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
return [active, incrementCount];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user