diff --git a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx index 5a97c28e9..50fa01b76 100644 --- a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx +++ b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx @@ -11,6 +11,7 @@ import CourseCalendarEvent, { CalendarEvent } from './CourseCalendarEvent'; import AppStore from '$stores/AppStore'; import locationIds from '$lib/location_ids'; import { useTimeFormatStore } from '$stores/SettingsStore'; +import { useHoveredStore } from '$stores/HoveredStore'; const localizer = momentLocalizer(moment); @@ -78,9 +79,14 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) { const [scheduleNames, setScheduleNames] = useState(AppStore.getScheduleNames()); const { isMilitaryTime } = useTimeFormatStore(); + const { hoveredCourseEvents } = useHoveredStore(); const getEventsForCalendar = () => { - return showFinalsSchedule ? finalsEventsInCalendar : eventsInCalendar; + return showFinalsSchedule + ? finalsEventsInCalendar + : hoveredCourseEvents + ? [...eventsInCalendar, ...hoveredCourseEvents] + : eventsInCalendar; }; const handleClosePopover = () => { @@ -129,7 +135,9 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) { // This equation is taken from w3c, does not use the colour difference part const minBrightnessDiff = 125; - const backgroundRegexResult = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(bg) as RegExpExecArray; // returns {hex, r, g, b} + const backgroundRegexResult = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec( + bg.slice(0, 7) + ) as RegExpExecArray; // returns {hex, r, g, b} const backgroundRGB = { r: parseInt(backgroundRegexResult[1], 16), g: parseInt(backgroundRegexResult[2], 16), diff --git a/apps/antalmanac/src/components/Header/SettingsMenu.tsx b/apps/antalmanac/src/components/Header/SettingsMenu.tsx index f4b72b6f9..d35dfae06 100644 --- a/apps/antalmanac/src/components/Header/SettingsMenu.tsx +++ b/apps/antalmanac/src/components/Header/SettingsMenu.tsx @@ -1,9 +1,10 @@ import { useCallback, useState } from 'react'; -import { Box, Button, ButtonGroup, Divider, Drawer, IconButton, Typography, useMediaQuery } from '@material-ui/core'; +import { Box, Button, ButtonGroup, Drawer, IconButton, Switch, Typography, useMediaQuery } from '@material-ui/core'; +import { Divider, Stack, Tooltip } from '@mui/material'; import { CSSProperties } from '@material-ui/core/styles/withStyles'; -import { Close, DarkMode, LightMode, Settings, SettingsBrightness } from '@mui/icons-material'; +import { Close, DarkMode, Help, LightMode, Settings, SettingsBrightness } from '@mui/icons-material'; -import { useThemeStore, useTimeFormatStore } from '$stores/SettingsStore'; +import { usePreviewStore, useThemeStore, useTimeFormatStore } from '$stores/SettingsStore'; const lightSelectedStyle: CSSProperties = { backgroundColor: '#F0F7FF', @@ -33,7 +34,7 @@ function ThemeMenu() { }; return ( - + Theme @@ -91,7 +92,7 @@ function TimeMenu() { }; return ( - + Time @@ -135,6 +136,30 @@ function TimeMenu() { ); } +function ExperimentalMenu() { + const [previewMode, setPreviewMode] = usePreviewStore((store) => [store.previewMode, store.setPreviewMode]); + + const handlePreviewChange = (event: React.ChangeEvent) => { + setPreviewMode(event.target.checked); + }; + + return ( + + + + + Hover to Preview + + Hover over courses to preview them in your calendar!}> + + + + + + + ); +} + function SettingsMenu() { const [drawerOpen, setDrawerOpen] = useState(false); const isMobileScreen = useMediaQuery('(max-width:750px)'); @@ -166,7 +191,7 @@ function SettingsMenu() { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - padding: '16px', + padding: '12px', }} > Settings @@ -174,10 +199,17 @@ function SettingsMenu() { + + + + Experimental Features + + + diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx index c2f23e89b..79cc5f140 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx @@ -33,7 +33,8 @@ import { useTabStore } from '$stores/TabStore'; import locationIds from '$lib/location_ids'; import { normalizeTime, parseDaysString, formatTimes } from '$stores/calendarizeHelpers'; import useColumnStore, { type SectionTableColumn } from '$stores/ColumnStore'; -import { useTimeFormatStore } from '$stores/SettingsStore'; +import { usePreviewStore, useTimeFormatStore } from '$stores/SettingsStore'; +import { useHoveredStore } from '$stores/HoveredStore'; const styles: Styles = (theme) => ({ sectionCode: { @@ -148,12 +149,12 @@ type SectionType = 'Act' | 'Col' | 'Dis' | 'Fld' | 'Lab' | 'Lec' | 'Qiz' | 'Res' interface SectionDetailCellProps { classes: ClassNameMap; sectionType: SectionType; - sectionNum: string; + sectionNumber: string; units: number; } const SectionDetailsCell = withStyles(styles)((props: SectionDetailCellProps) => { - const { classes, sectionType, sectionNum, units } = props; + const { classes, sectionType, sectionNumber, units } = props; const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT})`); return ( @@ -161,7 +162,7 @@ const SectionDetailsCell = withStyles(styles)((props: SectionDetailCellProps) => {sectionType} {!isMobileScreen && <>Sec: } - {sectionNum} + {sectionNumber} {!isMobileScreen && <>Units: } @@ -470,6 +471,7 @@ interface SectionTableBodyProps { scheduleNames: string[]; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any const tableBodyCells: Record> = { sectionCode: CourseCodeCell, sectionDetails: SectionDetailsCell, @@ -482,9 +484,6 @@ const tableBodyCells: Record> = { status: StatusCell, }; -/** - * TODO: SectionNum name parity -> SectionNumber - */ const SectionTableBody = withStyles(styles)((props: SectionTableBodyProps) => { const { classes, section, courseDetails, term, allowHighlight, scheduleNames } = props; @@ -505,20 +504,36 @@ const SectionTableBody = withStyles(styles)((props: SectionTableBodyProps) => { daysOccurring: parseDaysString(section.meetings[0].days), ...normalizeTime(section.meetings[0]), }; - }, [section.meetings[0]]); + }, [section.meetings]); // Stable references to event listeners will synchronize React state with the store. const updateHighlight = useCallback(() => { setAddedCourse(AppStore.getAddedSectionCodes().has(`${section.sectionCode} ${term}`)); - }, []); + }, [section.sectionCode, term]); const updateCalendarEvents = useCallback(() => { setCalendarEvents(AppStore.getCourseEventsInCalendar()); }, [setCalendarEvents]); - // Attach event listeners to the store. + const [hoveredCourseEvents, setHoveredCourseEvents] = useHoveredStore((store) => [ + store.hoveredCourseEvents, + store.setHoveredCourseEvents, + ]); + + const { previewMode } = usePreviewStore(); + + const handleHover = useCallback(() => { + const alreadyHovered = + hoveredCourseEvents && + hoveredCourseEvents.some((courseEvent) => courseEvent.sectionCode == section.sectionCode); + !previewMode || alreadyHovered || addedCourse + ? setHoveredCourseEvents(undefined) + : setHoveredCourseEvents(section, courseDetails, term); + }, [addedCourse, courseDetails, hoveredCourseEvents, previewMode, section, setHoveredCourseEvents, term]); + + // Attach event listeners to the store. useEffect(() => { AppStore.on('addedCoursesChange', updateHighlight); AppStore.on('currentScheduleIndexChange', updateHighlight); @@ -592,6 +607,8 @@ const SectionTableBody = withStyles(styles)((props: SectionTableBodyProps) => { // allowHighlight is ALWAYS false when in Added Course Pane and ALWAYS true when in CourseRenderPane addedCourse ? { addedCourse: addedCourse && allowHighlight } : { scheduleConflict: scheduleConflict } )} + onMouseEnter={handleHover} + onMouseLeave={handleHover} > {!addedCourse ? ( { ) : ( )} - {Object.entries(tableBodyCells) .filter(([column]) => activeColumns.includes(column as SectionTableColumn)) .map(([column, Component]) => { return ( - // All of this is a little bulky, so if the props can be added specifically to activeTableBodyColumns, LMK! void; +} + +export const useHoveredStore = create((set) => { + return { + hoveredCourseEvents: undefined, + setHoveredCourseEvents: (section, courseDetails, term) => { + set({ + hoveredCourseEvents: + section && courseDetails && term + ? calendarizeCourseEvents([ + { + ...courseDetails, + section: { + ...section, + color: '#80808080', + }, + term, + }, + ]) + : undefined, + }); + }, + }; +}); diff --git a/apps/antalmanac/src/stores/SettingsStore.ts b/apps/antalmanac/src/stores/SettingsStore.ts index 5ca0d6dd4..eab601688 100644 --- a/apps/antalmanac/src/stores/SettingsStore.ts +++ b/apps/antalmanac/src/stores/SettingsStore.ts @@ -20,8 +20,8 @@ export const useThemeStore = create((set) => { themeSetting !== 'system' ? themeSetting : window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'; + ? 'dark' + : 'light'; return { themeSetting: themeSetting as 'light' | 'dark' | 'system', @@ -35,8 +35,8 @@ export const useThemeStore = create((set) => { themeSetting !== 'system' ? themeSetting : window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'; + ? 'dark' + : 'light'; set({ appTheme: appTheme, themeSetting: themeSetting }); @@ -67,3 +67,22 @@ export const useTimeFormatStore = create((set) => { }, }; }); +export interface PreviewStore { + previewMode: boolean; + setPreviewMode: (previewMode: boolean) => void; +} + +export const usePreviewStore = create((set) => { + const previewMode = typeof Storage !== 'undefined' && window.localStorage.getItem('previewMode') == 'true'; + + return { + previewMode: previewMode, + setPreviewMode: (previewMode) => { + if (typeof Storage !== 'undefined') { + window.localStorage.setItem('previewMode', previewMode.toString()); + } + + set({ previewMode: previewMode }); + }, + }; +});