Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create hover view #836

Merged
merged 9 commits into from
Jan 24, 2024
16 changes: 12 additions & 4 deletions apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -52,8 +53,8 @@ const AntAlmanacEvent = ({ event }: { event: CalendarEvent }) => {
{event.showLocationInfo
? event.locations.map((location) => `${location.building} ${location.room}`).join(', ')
: event.locations.length > 1
? `${event.locations.length} Locations`
: `${event.locations[0].building} ${event.locations[0].room}`}
? `${event.locations.length} Locations`
: `${event.locations[0].building} ${event.locations[0].room}`}
</Box>
<Box>{event.sectionCode}</Box>
</Box>
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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),
Expand Down
44 changes: 38 additions & 6 deletions apps/antalmanac/src/components/Header/SettingsMenu.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -33,7 +34,7 @@ function ThemeMenu() {
};

return (
<Box sx={{ padding: '1rem 1rem 0 1rem', width: '100%' }}>
<Box sx={{ padding: '0 1rem', width: '100%' }}>
<Typography variant="h6" style={{ marginTop: '1.5rem', marginBottom: '1rem' }}>
Theme
</Typography>
Expand Down Expand Up @@ -91,7 +92,7 @@ function TimeMenu() {
};

return (
<Box sx={{ padding: '1rem 1rem 0 1rem', width: '100%' }}>
<Box sx={{ padding: '0 1rem', width: '100%' }}>
<Typography variant="h6" style={{ marginTop: '1.5rem', marginBottom: '1rem' }}>
Time
</Typography>
Expand Down Expand Up @@ -135,6 +136,30 @@ function TimeMenu() {
);
}

function ExperimentalMenu() {
const [previewMode, setPreviewMode] = usePreviewStore((store) => [store.previewMode, store.setPreviewMode]);

const handlePreviewChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPreviewMode(event.target.checked);
};

return (
<Stack sx={{ padding: '1rem 1rem 0 1rem', width: '100%', display: 'flex' }} alignItems="middle">
<Box display="flex" justifyContent="space-between" width={1}>
<Box display="flex" alignItems="center" style={{ gap: 4 }}>
<Typography variant="h6" style={{ display: 'flex', alignItems: 'center', alignContent: 'center' }}>
Hover to Preview
</Typography>
<Tooltip title={<Typography>Hover over courses to preview them in your calendar!</Typography>}>
<Help />
</Tooltip>
</Box>
<Switch color="primary" value={previewMode} checked={previewMode} onChange={handlePreviewChange} />
</Box>
</Stack>
);
}

function SettingsMenu() {
const [drawerOpen, setDrawerOpen] = useState(false);
const isMobileScreen = useMediaQuery('(max-width:750px)');
Expand Down Expand Up @@ -166,18 +191,25 @@ function SettingsMenu() {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
padding: '12px',
}}
>
<Typography variant="h6">Settings</Typography>
<IconButton size="medium" onClick={handleDrawerClose}>
<Close fontSize="inherit" />
</IconButton>
</Box>

<Divider />

<ThemeMenu />
<TimeMenu />

<Divider style={{ marginTop: '16px' }}>
<Typography variant="subtitle2">Experimental Features</Typography>
</Divider>

<ExperimentalMenu />
</Box>
</Drawer>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, object> = (theme) => ({
sectionCode: {
Expand Down Expand Up @@ -148,20 +149,20 @@ 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 (
<NoPaddingTableCell className={classes.cell} style={isMobileScreen ? { textAlign: 'center' } : {}}>
<Box className={classes[sectionType]}>{sectionType}</Box>
<Box>
{!isMobileScreen && <>Sec: </>}
{sectionNum}
{sectionNumber}
</Box>
<Box>
{!isMobileScreen && <>Units: </>}
Expand Down Expand Up @@ -470,6 +471,7 @@ interface SectionTableBodyProps {
scheduleNames: string[];
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tableBodyCells: Record<SectionTableColumn, React.ComponentType<any>> = {
sectionCode: CourseCodeCell,
sectionDetails: SectionDetailsCell,
Expand All @@ -482,9 +484,6 @@ const tableBodyCells: Record<SectionTableColumn, React.ComponentType<any>> = {
status: StatusCell,
};

/**
* TODO: SectionNum name parity -> SectionNumber
*/
const SectionTableBody = withStyles(styles)((props: SectionTableBodyProps) => {
const { classes, section, courseDetails, term, allowHighlight, scheduleNames } = props;

Expand All @@ -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);
Expand Down Expand Up @@ -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 ? (
<ScheduleAddCell
Expand All @@ -604,12 +621,10 @@ const SectionTableBody = withStyles(styles)((props: SectionTableBodyProps) => {
) : (
<ColorAndDelete color={section.color} sectionCode={section.sectionCode} term={term} />
)}

{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!
<Component
key={column}
section={section}
Expand Down
33 changes: 33 additions & 0 deletions apps/antalmanac/src/stores/HoveredStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { create } from 'zustand';
import { AASection } from '@packages/antalmanac-types';
import { calendarizeCourseEvents } from './calendarizeHelpers';
import { CourseEvent } from '$components/Calendar/CourseCalendarEvent';
import { CourseDetails } from '$lib/course_data.types';

export interface HoveredStore {
hoveredCourseEvents: CourseEvent[] | undefined;
setHoveredCourseEvents: (section?: AASection, courseDetails?: CourseDetails, term?: string) => void;
}

export const useHoveredStore = create<HoveredStore>((set) => {
return {
hoveredCourseEvents: undefined,
setHoveredCourseEvents: (section, courseDetails, term) => {
set({
hoveredCourseEvents:
section && courseDetails && term
? calendarizeCourseEvents([
{
...courseDetails,
section: {
...section,
color: '#80808080',
},
term,
},
])
: undefined,
});
},
};
});
27 changes: 23 additions & 4 deletions apps/antalmanac/src/stores/SettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export const useThemeStore = create<ThemeStore>((set) => {
themeSetting !== 'system'
? themeSetting
: window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
? 'dark'
: 'light';

return {
themeSetting: themeSetting as 'light' | 'dark' | 'system',
Expand All @@ -35,8 +35,8 @@ export const useThemeStore = create<ThemeStore>((set) => {
themeSetting !== 'system'
? themeSetting
: window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
? 'dark'
: 'light';

set({ appTheme: appTheme, themeSetting: themeSetting });

Expand Down Expand Up @@ -67,3 +67,22 @@ export const useTimeFormatStore = create<TimeFormatStore>((set) => {
},
};
});
export interface PreviewStore {
previewMode: boolean;
setPreviewMode: (previewMode: boolean) => void;
}

export const usePreviewStore = create<PreviewStore>((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 });
},
};
});
Loading