diff --git a/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx b/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx index 642a2d283..9a5c38e22 100644 --- a/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx +++ b/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx @@ -171,7 +171,7 @@ const CourseCalendarEvent = (props: CourseCalendarEventProps) => { const isDark = useThemeStore((store) => store.isDark); const focusMap = useCallback(() => { - setActiveTab(2); + setActiveTab('map'); }, [setActiveTab]); const { classes, selectedEvent } = props; diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx index 7842f4469..5b276fbcd 100644 --- a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx +++ b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx @@ -363,7 +363,7 @@ function AddedSectionsGrid() { ); } -export default function AddedCoursePaneFunctionComponent() { +export function AddedCoursePane() { const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); useEffect(() => { diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx index 1eb22a82d..8169a95a4 100644 --- a/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx +++ b/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx @@ -60,7 +60,7 @@ const CustomEventDetailView = (props: CustomEventDetailViewProps) => { const { setActiveTab } = useTabStore(); const focusMap = useCallback(() => { - setActiveTab(2); + setActiveTab('map'); }, [setActiveTab]); return ( diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/ActionCell.tsx similarity index 94% rename from apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx rename to apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/ActionCell.tsx index 92da3434d..f7819e069 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/ActionCell.tsx @@ -3,7 +3,7 @@ import { Box, IconButton, Menu, MenuItem, TableCell, Tooltip, useMediaQuery } fr import { AASection, CourseDetails } from '@packages/antalmanac-types'; import { bindMenu, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks'; -import { MOBILE_BREAKPOINT } from '../../../../globals'; +import { MOBILE_BREAKPOINT } from '../../../../../globals'; import { addCourse, deleteCourse, openSnackbar } from '$actions/AppStoreActions'; import ColorPicker from '$components/ColorPicker'; @@ -13,7 +13,7 @@ import AppStore from '$stores/AppStore'; /** * Props received by components that perform actions on a specified section. */ -interface SectionActionProps { +interface ActionProps { /** * The section to perform actions on. */ @@ -43,7 +43,7 @@ interface SectionActionProps { /** * Sections added to a schedule, can be recolored or deleted. */ -export function ColorAndDelete(props: SectionActionProps) { +export function ColorAndDelete(props: ActionProps) { const { section, term } = props; const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}`); @@ -86,7 +86,7 @@ const fieldsToReset = ['courseCode', 'courseNumber', 'deptLabel', 'deptValue', ' /** * Sections that have not been added to a schedule can be added to a schedule. */ -export function ScheduleAddCell(props: SectionActionProps) { +export function ScheduleAddCell(props: ActionProps) { const { section, courseDetails, term, scheduleNames, scheduleConflict } = props; const popupState = usePopupState({ popupId: 'SectionTableAddCellPopup', variant: 'popover' }); @@ -168,7 +168,7 @@ export function ScheduleAddCell(props: SectionActionProps) { ); } -export interface SectionActionCellProps extends Omit { +export interface ActionCellProps extends Omit { /** * Whether the section has been added. */ @@ -178,7 +178,7 @@ export interface SectionActionCellProps extends Omit {props.addedCourse ? : } diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/LocationsCell.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/LocationsCell.tsx index bafacd1b9..a40619340 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/LocationsCell.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/LocationsCell.tsx @@ -18,7 +18,7 @@ export const LocationsCell = ({ meetings }: LocationsCellProps) => { const { setActiveTab } = useTabStore(); const focusMap = useCallback(() => { - setActiveTab(2); + setActiveTab('map'); }, [setActiveTab]); return ( diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyRow.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyRow.tsx index d0d63e8e0..005531841 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyRow.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody/SectionTableBodyRow.tsx @@ -2,7 +2,7 @@ import { TableRow, useTheme } from '@mui/material'; import { AASection, CourseDetails } from '@packages/antalmanac-types'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { SectionActionCell } from '../cells/action'; +import { ActionCell } from './SectionTableBodyCells/ActionCell'; import { CourseCodeCell } from '$components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/CourseCodeCell'; import { DayAndTimeCell } from '$components/RightPane/SectionTable/SectionTableBody/SectionTableBodyCells/DayAndTimeCell'; @@ -30,7 +30,7 @@ interface SectionTableBodyRowProps { // These components have too varied of types, any is fine here // eslint-disable-next-line @typescript-eslint/no-explicit-any const tableBodyCells: Record> = { - action: SectionActionCell, + action: ActionCell, sectionCode: CourseCodeCell, sectionDetails: DetailsCell, instructors: InstructorsCell, diff --git a/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagement.tsx b/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagement.tsx new file mode 100644 index 000000000..1b11c8191 --- /dev/null +++ b/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagement.tsx @@ -0,0 +1,103 @@ +import { GlobalStyles, Stack, useMediaQuery, useTheme } from '@mui/material'; +import { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import { ScheduleManagementTabs } from '$components/ScheduleManagement/ScheduleManagementTabs'; +import { ScheduleManagementTabsContent } from '$components/ScheduleManagement/ScheduleManagementTabsContent'; +import { getLocalStorageUserId } from '$lib/localStorage'; +import { useTabStore } from '$stores/TabStore'; + +/** + * List of interactive tab buttons with their accompanying content. + * Each tab's content has functionality for managing the user's schedule. + */ +export function ScheduleManagement() { + const { activeTab, setActiveTab } = useTabStore(); + const { tab } = useParams(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + // Tab index mapped to the last known scrollTop. + const [positions, setPositions] = useState>({}); + + /** + * Ref to the scrollable container with all of the tabs-content within it. + */ + const ref = useRef(null); + + // Save the current scroll position to the store. + const onScroll = (e: React.UIEvent) => { + const positionToSave = e.currentTarget.scrollTop; + setPositions((current) => { + current[activeTab] = positionToSave; + return current; + }); + }; + + // Change the tab to the "added classes" tab if the user was previously logged in. + useEffect(() => { + if (tab) { + switch (tab) { + case 'added': + setActiveTab('added'); + break; + case 'map': + setActiveTab('map'); + break; + } + + return; + } + + const userId = getLocalStorageUserId(); + + if (userId === null) { + setActiveTab('search'); + } else if (isMobile) { + setActiveTab('calendar'); + } else { + setActiveTab('added'); + } + }, [setActiveTab]); + + // Restore scroll position if it has been previously saved. + useEffect(() => { + const savedPosition = positions[activeTab]; + + const animationFrame = requestAnimationFrame(() => { + if (ref.current && savedPosition != null) { + ref.current.scrollTop = savedPosition; + } + }); + + return () => { + if (animationFrame != null) { + cancelAnimationFrame(animationFrame); + } + }; + }, [activeTab, positions]); + + return ( + + + + {!isMobile && } + + + + + + + + {isMobile && } + + ); +} diff --git a/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagementTabs.tsx b/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagementTabs.tsx new file mode 100644 index 000000000..47b21a461 --- /dev/null +++ b/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagementTabs.tsx @@ -0,0 +1,107 @@ +import { Event, FormatListBulleted, MyLocation, Search } from '@mui/icons-material'; +import { Paper, Tab, Tabs, useMediaQuery, useTheme } from '@mui/material'; +import { Link } from 'react-router-dom'; + +import { useThemeStore } from '$stores/SettingsStore'; +import { useTabStore } from '$stores/TabStore'; + +/** + * Information about the tab navigation buttons. + * + * Each button should be associated with a different aspect of schedule management. + */ +export type ScheduleManagementTabInfo = { + /** + * Label to display on the tab button. + */ + label: string; + + /** + * The path to navigate to in the URL. + */ + href: string; + + /** + * Icon to display. + */ + icon: React.ReactElement; + + /** + * ID for the tab? + */ + id?: string; + + /** + * Whether or not this is mobile-only. + */ + mobile?: true; +}; + +const scheduleManagementTabs: Array = [ + { + label: 'Calendar', + icon: , + mobile: true, + href: '', + }, + { + label: 'Search', + href: '/', + icon: , + }, + { + label: 'Added', + href: '/added', + icon: , + id: 'added-courses-tab', + }, + { + label: 'Map', + href: '/map', + icon: , + id: 'map-tab', + }, +]; + +export function ScheduleManagementTabs() { + const { activeTab, setActiveTabValue } = useTabStore(); + const isDark = useThemeStore((store) => store.isDark); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const onChange = (_event: React.SyntheticEvent, value: number) => { + setActiveTabValue(value); + }; + + return ( + + + {scheduleManagementTabs.map((tab) => { + return ( + + ); + })} + + + ); +} diff --git a/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagementTabsContent.tsx b/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagementTabsContent.tsx new file mode 100644 index 000000000..183c0cb4f --- /dev/null +++ b/apps/antalmanac/src/components/ScheduleManagement/ScheduleManagementTabsContent.tsx @@ -0,0 +1,50 @@ +import { lazy, Suspense } from 'react'; + +import darkModeLoadingGif from '../RightPane/CoursePane/SearchForm/Gifs/dark-loading.gif'; +import loadingGif from '../RightPane/CoursePane/SearchForm/Gifs/loading.gif'; + +import { ScheduleCalendar } from '$components/Calendar/CalendarRoot'; +import { AddedCoursePane } from '$components/RightPane/AddedCourses/AddedCoursePane'; +import { CoursePaneRoot } from '$components/RightPane/CoursePane/CoursePaneRoot'; +import { useThemeStore } from '$stores/SettingsStore'; +import { useTabStore } from '$stores/TabStore'; + + +const UCIMap = lazy(() => import('../Map/Map')); + +export function ScheduleManagementTabsContent() { + const { activeTab } = useTabStore(); + const isDark = useThemeStore((store) => store.isDark); + + switch (activeTab) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ( + + Loading map + + } + > + + + ); + + default: + return null; + } +} diff --git a/apps/antalmanac/src/components/SharedRoot.tsx b/apps/antalmanac/src/components/SharedRoot.tsx deleted file mode 100644 index e8a647ed7..000000000 --- a/apps/antalmanac/src/components/SharedRoot.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { Event, FormatListBulleted, MyLocation, Search } from '@mui/icons-material'; -import { GlobalStyles, Paper, Stack, Tab, Tabs, Typography, useMediaQuery, useTheme } from '@mui/material'; -import { Suspense, lazy, useEffect, useRef, useState } from 'react'; -import { Link, useParams } from 'react-router-dom'; - -import { ScheduleCalendar } from './Calendar/CalendarRoot'; -import AddedCoursePane from './RightPane/AddedCourses/AddedCoursePane'; -import darkModeLoadingGif from './RightPane/CoursePane/SearchForm/Gifs/dark-loading.gif'; -import loadingGif from './RightPane/CoursePane/SearchForm/Gifs/loading.gif'; - -import { CoursePaneRoot } from '$components/RightPane/CoursePane/CoursePaneRoot'; -import { getLocalStorageUserId } from '$lib/localStorage'; -import { useThemeStore } from '$stores/SettingsStore'; -import { useTabStore } from '$stores/TabStore'; - -const UCIMap = lazy(() => import('./Map/Map')); - -/** - * Information about the tab navigation buttons. - * - * Each button should be associated with a different aspect of schedule management. - */ -type ScheduleManagementTabInfo = { - /** - * Label to display on the tab button. - */ - label: string; - - /** - * The path to navigate to in the URL. - */ - href: string; - - /** - * Icon to display. - */ - icon: React.ElementType; - - /** - * ID for the tab? - */ - id?: string; - - /** - * Whether or not this is mobile-only. - */ - mobile?: true; -}; - -/** - */ -const scheduleManagementTabs: Array = [ - { - label: 'Calendar', - icon: Event, - mobile: true, - href: '', - }, - { - label: 'Search', - href: '/', - icon: Search, - }, - { - label: 'Added', - href: '/added', - icon: FormatListBulleted, - id: 'added-courses-tab', - }, - { - label: 'Map', - href: '/map', - icon: MyLocation, - id: 'map-tab', - }, -]; - -/** - * A different set of tab buttons will be listed depending on whether the screen is mobile or - * desktop. - * - * Provide the current state of the tab navigation from the parent. - */ -type ScheduleManagementTabsProps = { - value: number; - setActiveTab: (value: number) => void; -}; - -/** - * For mobile devices, all tabs will be displayed. - */ -function ScheduleManagementMobileTabs(props: ScheduleManagementTabsProps) { - const { value, setActiveTab } = props; - const isDark = useThemeStore((store) => store.isDark); - - const onChange = (_event: React.SyntheticEvent, value: number) => { - setActiveTab(value); - }; - - return ( - - {scheduleManagementTabs.map((tab) => ( - - - - {tab.label} - - - } - /> - ))} - - ); -} - -/** - * For desktop, some of the tabs will be displayed on the other side. - * i.e. the calendar takes up the left side of the screen. - */ -function ScheduleManagementDesktopTabs(props: ScheduleManagementTabsProps) { - const { value, setActiveTab } = props; - const isDark = useThemeStore((store) => store.isDark); - - const onChange = (_event: React.SyntheticEvent, value: number) => { - setActiveTab(value + 1); - }; - - return ( - - {scheduleManagementTabs.map((tab) => { - if (tab.mobile) return; - - return ( - - - {tab.label} - - } - /> - ); - })} - - ); -} - -function ScheduleManagementTabsContent(props: { activeTab: number; isMobile: boolean }) { - const { activeTab } = props; - - const isDark = useThemeStore((store) => store.isDark); - - switch (activeTab) { - case 0: - return ; - case 1: - return ; - case 2: - return ; - case 3: - return ( - - Loading map - - } - > - - - ); - - default: - return null; - } -} - -/** - * List of interactive tab buttons with their accompanying content. - * Each tab's content has functionality for managing the user's schedule. - */ -export default function ScheduleManagement() { - const theme = useTheme(); - - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - - const { activeTab, setActiveTab } = useTabStore(); - - const { tab } = useParams(); - - // Tab index mapped to the last known scrollTop. - const [positions, setPositions] = useState>({}); - - /** - * Ref to the scrollable container with all of the tabs-content within it. - */ - const ref = useRef(); - - const value = isMobile ? activeTab : activeTab - 1 >= 0 ? activeTab - 1 : 0; - - // Save the current scroll position to the store. - const onScroll = (e: React.UIEvent) => { - const positionToSave = e.currentTarget.scrollTop; - setPositions((current) => { - current[activeTab] = positionToSave; - return current; - }); - }; - - // Change the tab to the "added classes" tab if the user was previously logged in. - useEffect(() => { - const userId = getLocalStorageUserId(); - - if (userId != null) { - setActiveTab(2); - } else { - setActiveTab(1); - } - }, [setActiveTab]); - - // Handle tab index for mobile screens. - useEffect(() => { - if (isMobile) return; - - if (tab === 'map') { - setActiveTab(3); - } - - if (activeTab == 0) { - setActiveTab(1); - } - }, [activeTab, isMobile, setActiveTab, tab]); - - // Restore scroll position if it has been previously saved. - useEffect(() => { - const savedPosition = positions[activeTab]; - - let animationFrame: number; - - if (savedPosition != null) { - animationFrame = requestAnimationFrame(() => { - if (ref.current) { - ref.current.scrollTop = savedPosition; - } - }); - } - - return () => { - if (animationFrame != null) { - cancelAnimationFrame(animationFrame); - } - }; - }, [activeTab, positions]); - - if (activeTab === 0 && !isMobile) { - return ; - } - - return ( - - - - {!isMobile && ( - - - - )} - - - - - - - - {isMobile && ( - - - - )} - - ); -} diff --git a/apps/antalmanac/src/lib/TutorialHelpers.tsx b/apps/antalmanac/src/lib/TutorialHelpers.tsx index 4e2655897..baa77d252 100644 --- a/apps/antalmanac/src/lib/TutorialHelpers.tsx +++ b/apps/antalmanac/src/lib/TutorialHelpers.tsx @@ -55,7 +55,7 @@ function KbdCard(props: { children?: React.ReactNode }) { } export function namedStepsFactory(goToStep: (step: number) => void): Record { - const setTab = useTabStore.getState().setActiveTab; + const setActiveTab = useTabStore.getState().setActiveTab; const goToNamedStep = (stepName: TourStepName) => { const stepIndex = tourStepNames.findIndex((step) => step == stepName); @@ -91,7 +91,7 @@ export function namedStepsFactory(goToStep: (step: number) => void): Record { markTourHasRun(); - setTab(0); + setActiveTab('search'); }, mutationObservables: ['#searchBar'], }, @@ -161,7 +161,7 @@ export function namedStepsFactory(goToStep: (step: number) => void): RecordSelect the added courses tab for a list of your courses and details ), - action: () => setTab(1), + action: () => setActiveTab('added'), mutationObservables: ['#course-pane-box'], }, map: { @@ -179,7 +179,7 @@ export function namedStepsFactory(goToStep: (step: number) => void): Record setTab(2), + action: () => setActiveTab('map'), mutationObservables: ['#map-pane'], }, saveAndLoad: { diff --git a/apps/antalmanac/src/lib/helpers.ts b/apps/antalmanac/src/lib/helpers.ts index b2cb54d99..7b643594b 100644 --- a/apps/antalmanac/src/lib/helpers.ts +++ b/apps/antalmanac/src/lib/helpers.ts @@ -43,7 +43,7 @@ export function useQuickSearchForClasses() { RightPaneStore.updateFormValue('courseNumber', courseNumber); RightPaneStore.updateFormValue('term', termValue); navigate(href, { replace: false }); - setActiveTab(1); + setActiveTab('search'); displaySections(); forceUpdate(); }, diff --git a/apps/antalmanac/src/routes/Home.tsx b/apps/antalmanac/src/routes/Home.tsx index e326eef86..b3ce2a27c 100644 --- a/apps/antalmanac/src/routes/Home.tsx +++ b/apps/antalmanac/src/routes/Home.tsx @@ -8,7 +8,7 @@ import { ScheduleCalendar } from '$components/Calendar/CalendarRoot'; import Header from '$components/Header'; import NotificationSnackbar from '$components/NotificationSnackbar'; import PatchNotes from '$components/PatchNotes'; -import ScheduleManagement from '$components/SharedRoot'; +import { ScheduleManagement } from '$components/ScheduleManagement/ScheduleManagement'; import { Tutorial } from '$components/Tutorial'; import { useScheduleManagementStore } from '$stores/ScheduleManagementStore'; @@ -25,7 +25,7 @@ function DesktopHome() { const theme = useTheme(); const setScheduleManagementWidth = useScheduleManagementStore((state) => state.setScheduleManagementWidth); - const scheduleManagementRef = useRef(); + const scheduleManagementRef = useRef(null); const handleDrag = useCallback(() => { const scheduleManagementElement = scheduleManagementRef.current; diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index be02dcb9f..1f37a4c09 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -359,7 +359,7 @@ class AppStore extends EventEmitter { this.emit('skeletonModeChange'); // Switch to added courses tab since Anteater API can't be reached anyway - useTabStore.getState().setActiveTab(2); + useTabStore.getState().setActiveTab('added'); } changeCurrentSchedule(newScheduleIndex: number) { diff --git a/apps/antalmanac/src/stores/TabStore.ts b/apps/antalmanac/src/stores/TabStore.ts index 61c756327..95125f0fa 100644 --- a/apps/antalmanac/src/stores/TabStore.ts +++ b/apps/antalmanac/src/stores/TabStore.ts @@ -1,20 +1,53 @@ import { create } from 'zustand'; +type TabName = 'calendar' | 'search' | 'added' | 'map'; + interface TabStore { activeTab: number; - setActiveTab: (newTab: number) => void; + + /** + * Sets the appropriate tab value given a string literal union + */ + setActiveTab: (name: TabName) => void; + + /** + * Sets the appropriate tab value given a tab index. + * + * `setActiveTab` (which accepts a string literal union) should be preferred if trying to change tabs programmatically (and not as part of an event handler) + */ + setActiveTabValue: (value: number) => void; } export const useTabStore = create((set) => { - const pathArray = typeof window !== 'undefined' ? window.location.pathname.split('/').slice(1) : []; - const tabName = pathArray[0]; - return { - activeTab: tabName === 'added' ? 1 : tabName === 'map' ? 2 : 0, - setActiveTab: (newTab: number) => { - set(() => ({ - activeTab: newTab, - })); + activeTab: 1, + setActiveTab: (name: TabName) => { + if (name === 'calendar') { + set(() => ({ + activeTab: 0, + })); + } + + if (name === 'search') { + set(() => ({ + activeTab: 1, + })); + } + + if (name === 'added') { + set(() => ({ + activeTab: 2, + })); + } + + if (name === 'map') { + set(() => ({ + activeTab: 3, + })); + } + }, + setActiveTabValue: (value: number) => { + set(() => ({ activeTab: value })); }, }; });