Skip to content

Commit

Permalink
feat: create hover view (#836)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinWu098 authored Jan 24, 2024
1 parent af8e096 commit 08a9bdc
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 24 deletions.
12 changes: 10 additions & 2 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 @@ -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 });
},
};
});

0 comments on commit 08a9bdc

Please sign in to comment.