import addCourse from '@pages/background/lib/addCourse'; import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule'; import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; // import { getCourseColors } from '@shared/util/colors'; // import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import { Button } from '@views/components/common/Button'; import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider'; 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'; 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 { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; import clsx from 'clsx'; import React, { useCallback, useEffect, useState } from 'react'; import IconoirGitFork from '~icons/iconoir/git-fork'; // import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories'; import DeleteForeverIcon from '~icons/material-symbols/delete-forever'; import { useMigrationDialog } from '../common/MigrationDialog'; // import RefreshIcon from '~icons/material-symbols/refresh'; import DevMode from './DevMode'; import Preview from './Preview'; const DISPLAY_PREVIEWS = false; const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4'; const manifest = chrome.runtime.getManifest(); const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href; const gitHubStatsService = new GitHubStatsService(); const includeMergedPRs = false; /** * 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. * * @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 showMigrationDialog = useMigrationDialog(); // Toggle GitHub stats when the user presses the 'S' key const [showGitHubStats, setShowGitHubStats] = useState(false); const [githubStats, setGitHubStats] = useState > | null>(null); const [activeSchedule] = useSchedules(); // const [isRefreshing, setIsRefreshing] = useState(false); const showDialog = usePrompt(); const handleChangelogOnClick = useChangelog(); useEffect(() => { const fetchGitHubStats = async () => { try { const stats = await gitHubStatsService.fetchGitHubStats(); setGitHubStats(stats); } catch (error) { console.warn('Error fetching GitHub stats:', error); } }; const initAndSetSettings = async () => { const { enableCourseStatusChips, enableTimeAndLocationInPopup, enableHighlightConflicts, enableScrollToLoad, enableDataRefreshing, } = await initSettings(); setEnableCourseStatusChips(enableCourseStatusChips); setShowTimeLocation(enableTimeAndLocationInPopup); setHighlightConflicts(enableHighlightConflicts); setLoadAllCourses(enableScrollToLoad); setEnableDataRefreshing(enableDataRefreshing); }; 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 l3 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => { setHighlightConflicts(newValue); // console.log('enableHighlightConflicts', newValue); }); const l4 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => { setLoadAllCourses(newValue); // console.log('enableScrollToLoad', newValue); }); const l5 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { setEnableDataRefreshing(newValue); // console.log('enableDataRefreshing', newValue); }); // Remove listeners when the component is unmounted return () => { OptionsStore.removeListener(l1); OptionsStore.removeListener(l2); OptionsStore.removeListener(l3); OptionsStore.removeListener(l4); OptionsStore.removeListener(l5); window.removeEventListener('keydown', handleKeyPress); }; }, []); const handleEraseAll = () => { showDialog({ title: 'Erase All Course/Schedule Data', description: ( <>

Are you sure you want to erase all schedules and courses you have? This action is permanent and cannot be undone.


Note: This will not erase your settings and preferences.

), // eslint-disable-next-line react/no-unstable-nested-components buttons: accept => ( ), }); }; // todo: move into a util/shared place, rather than specifically in settings const handleAddCourseByUrl = async () => { // todo: Use a proper modal instead of a prompt // eslint-disable-next-line no-alert const link: string | null = prompt('Enter course link'); // Exit if the user cancels the prompt if (link === null) return; try { let response: Response; try { response = await fetch(link); } catch (e) { // eslint-disable-next-line no-alert alert(`Failed to fetch url '${link}'`); return; } const text = await response.text(); const doc = new DOMParser().parseFromString(text, 'text/html'); const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS, doc, link); const tableRows = getCourseTableRows(doc); const courses = scraper.scrape(tableRows, false); if (courses.length === 1) { const description = scraper.getDescription(doc); const row = courses[0]!; const course = row.course!; course.description = description; // console.log(course); if (activeSchedule.courses.every(c => c.uniqueId !== course.uniqueId)) { console.log('adding course'); addCourse(activeSchedule.id, course); } else { console.log('course already exists'); } } else { console.log(courses); } } catch (error) { console.error('Error scraping course:', error); } }; const [devMode, toggleDevMode] = useDevMode(10); if (devMode) { return ; } return (
UTRP SETTINGS & CREDITS PAGE
LD Icon
{/*

CUSTOMIZATION OPTIONS

Show Course Status

Shows an indicator for waitlisted, cancelled, and closed courses.

{ setEnableCourseStatusChips(!enableCourseStatusChips); OptionsStore.set('enableCourseStatusChips', !enableCourseStatusChips); }} />

Show Time & Location in Popup

Shows the course's time and location in the extension's popup.

{ setShowTimeLocation(!showTimeLocation); OptionsStore.set('enableTimeAndLocationInPopup', !showTimeLocation); }} />
{DISPLAY_PREVIEWS && ( )}
*/}

ADVANCED SETTINGS

{/*

Refresh Data

Refreshes waitlist, course status, and other info with the latest data from UT's site.

*/}
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); }} />
Reset All Data

Erases all schedules and courses you have.

{DISPLAY_PREVIEWS && (
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
01234 MWF 10:00 AM - 11:00 AM UTC 1.234
)}

Developer Mode

LONGHORN DEVELOPERS ADMINS

{LONGHORN_DEVELOPERS_ADMINS.map(admin => (
window.open(`https://github.com/${admin.githubUsername}`, '_blank') } > {admin.name}

{admin.role}

{showGitHubStats && githubStats && (

GitHub Stats (UTRP repo):

{includeMergedPRs && (

Merged PRS:{' '} {githubStats.adminGitHubStats[admin.githubUsername]?.mergedPRs}

)}

Commits: {githubStats.adminGitHubStats[admin.githubUsername]?.commits}

{githubStats.adminGitHubStats[admin.githubUsername]?.linesAdded} ++

{githubStats.adminGitHubStats[admin.githubUsername]?.linesDeleted} --

)}
))}

UTRP CONTRIBUTORS

{LONGHORN_DEVELOPERS_SWE.sort( (a, b) => (githubStats?.userGitHubStats[b.githubUsername]?.commits ?? 0) - (githubStats?.userGitHubStats[a.githubUsername]?.commits ?? 0) ).map(swe => (
window.open(`https://github.com/${swe.githubUsername}`, '_blank') } > {swe.name}

{swe.role}

{showGitHubStats && githubStats && (

GitHub Stats (UTRP repo):

{includeMergedPRs && (

Merged PRS:{' '} {githubStats.userGitHubStats[swe.githubUsername]?.mergedPRs}

)}

Commits: {githubStats.userGitHubStats[swe.githubUsername]?.commits}

{githubStats.userGitHubStats[swe.githubUsername]?.linesAdded} ++

{githubStats.userGitHubStats[swe.githubUsername]?.linesDeleted} --

)}
))} {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 => (
window.open(`https://github.com/${username}`, '_blank')} > {githubStats.names[username]}

Contributor

{showGitHubStats && (

GitHub Stats (UTRP repo):

{includeMergedPRs && (

Merged PRs:{' '} {githubStats.userGitHubStats[username]?.mergedPRs}

)}

Commits: {githubStats.userGitHubStats[username]?.commits}

{githubStats.userGitHubStats[username]?.linesAdded} ++

{githubStats.userGitHubStats[username]?.linesDeleted} --

)}
))}
); }