diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 3b3d4a12..e75ca4ac 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -3,6 +3,7 @@ import React, { useContext, useMemo } from 'react'; import { ScheduleContext, TermsContext } from '../../contexts'; import HeaderDisplay from '../HeaderDisplay'; import useHeaderActionBarProps from '../../hooks/useHeaderActionBarProps'; +import { Term } from '../../types'; import './stylesheet.scss'; @@ -14,6 +15,24 @@ export type HeaderProps = { captureRef: React.RefObject; }; +type VersionState = { + type: 'loaded'; + currentVersion: string; + allVersionNames: readonly { id: string; name: string }[]; + setCurrentVersion: (next: string) => void; + addNewVersion: (name: string, select?: boolean) => string; + deleteVersion: (id: string) => void; + renameVersion: (id: string, newName: string) => void; + cloneVersion: (id: string, newName: string) => void; +}; + +type TermsState = { + type: 'loaded'; + terms: Term[]; + currentTerm: string; + onChangeTerm: (next: string) => void; +}; + /** * Renders the top header component with all state/interactivity, * and includes controls for top-level tab-based navigation. @@ -47,6 +66,37 @@ export default function Header({ }, [pinnedCrns, oscar]); const headerActionBarProps = useHeaderActionBarProps(captureRef); + const termsState = useMemo( + () => ({ + type: 'loaded', + terms, + currentTerm: term, + onChangeTerm: setTerm, + }), + [setTerm, term, terms] + ) as TermsState; + + const versionsState = useMemo( + () => ({ + type: 'loaded', + allVersionNames, + currentVersion, + setCurrentVersion, + addNewVersion, + deleteVersion, + renameVersion, + cloneVersion, + }), + [ + addNewVersion, + allVersionNames, + cloneVersion, + currentVersion, + deleteVersion, + renameVersion, + setCurrentVersion, + ] + ) as VersionState; return ( ); diff --git a/src/components/HeaderDisplay/index.tsx b/src/components/HeaderDisplay/index.tsx index d80b0ab5..5ae7aafb 100644 --- a/src/components/HeaderDisplay/index.tsx +++ b/src/components/HeaderDisplay/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBars, @@ -18,7 +18,7 @@ import HeaderActionBar from '../HeaderActionBar'; import Modal from '../Modal'; import { AccountContextValue } from '../../contexts/account'; import { Term } from '../../types'; -import Toast from '../Toast'; +import Toast, { notifyToast } from '../Toast'; import './stylesheet.scss'; @@ -35,6 +35,15 @@ type VersionState = cloneVersion: (id: string, newName: string) => void; }; +type TermsState = + | { type: 'loading' } + | { + type: 'loaded'; + terms: Term[]; + currentTerm: string; + onChangeTerm: (next: string) => void; + }; + export type HeaderDisplayProps = { totalCredits?: number | null; currentTab: number; @@ -47,14 +56,7 @@ export type HeaderDisplayProps = { enableExportCalendar?: boolean; onDownloadCalendar?: () => void; enableDownloadCalendar?: boolean; - termsState: - | { type: 'loading' } - | { - type: 'loaded'; - terms: Term[]; - currentTerm: string; - onChangeTerm: (next: string) => void; - }; + termsState: TermsState; versionsState: VersionState; accountState: AccountContextValue | { type: 'loading' }; skeleton: boolean; @@ -92,17 +94,17 @@ export default function HeaderDisplay({ // (small mobile is < 600 px wide) const largeMobile = useScreenWidth(LARGE_MOBILE_BREAKPOINT); - // useEffect(() => { - // if (termsState.type === 'loaded' && !skeleton) { - // const termObject = termsState.terms.filter( - // (term) => term.term === termsState.currentTerm - // )[0]; + useEffect(() => { + if (termsState.type === 'loaded' && !skeleton) { + const termObject = termsState.terms.filter( + (term) => term.term === termsState.currentTerm + )[0]; - // if (!termObject?.finalized) { - // notifyToast('finalized-term-toast'); - // } - // } - // }); + if (!termObject?.finalized) { + notifyToast('finalized-term-toast'); + } + } + }, [termsState, skeleton]); return (
diff --git a/src/data/hooks/useUIStateFromStorage.ts b/src/data/hooks/useUIStateFromStorage.ts index 7d62b5fe..ff357e11 100644 --- a/src/data/hooks/useUIStateFromStorage.ts +++ b/src/data/hooks/useUIStateFromStorage.ts @@ -1,6 +1,5 @@ import { useCallback } from 'react'; - -import useLocalStorageNoSync from '../../hooks/useLocalStorageNoSync'; +import useLocalStorageState from 'use-local-storage-state'; export const UI_STATE_LOCAL_STORAGE_KEY = 'ui-state'; @@ -36,9 +35,12 @@ type HookResult = { * but still have the app resume to the last viewed schedule when opened again. */ export default function useUIStateFromStorage(): HookResult { - const [{ currentTerm, versionStates }, setUIState] = useLocalStorageNoSync( + const [{ currentTerm, versionStates }, setUIState] = useLocalStorageState( UI_STATE_LOCAL_STORAGE_KEY, - defaultUIState + { + defaultValue: defaultUIState, + storageSync: false, + } ); const setTerm = useCallback( diff --git a/src/hooks/useLocalStorageNoSync.ts b/src/hooks/useLocalStorageNoSync.ts deleted file mode 100644 index b9051b9f..00000000 --- a/src/hooks/useLocalStorageNoSync.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useState } from 'react'; - -import { ErrorWithFields, softError } from '../log'; - -export type HookResult = [T, (value: T | ((curr: T) => T)) => void]; - -/** - * From https://usehooks.com/useLocalStorage/. - * Similar to `use-local-storage-state` (which should be preferred), - * except it does not sync the state between browser tabs. - * - * TODO(jazeved0): `use-local-storage-state` now supports `storageSync: false`, - * so this hook is no longer necessary. - * - * @deprecated Use `use-local-storage-state` instead. - */ -export default function useLocalStorageNoSync( - key: string, - initialValue: T -): HookResult { - const [storedValue, setStoredValue] = useState(() => { - let item: string | null = null; - try { - item = window.localStorage.getItem(key); - return item ? (JSON.parse(item) as T) : initialValue; - } catch (error) { - softError( - new ErrorWithFields({ - message: 'useLocalStorageNoSync load local storage failed', - source: error, - fields: { - key, - }, - }) - ); - return initialValue; - } - }); - - const setValue = (value: T | ((val: T) => T)): void => { - try { - const valueToStore = - value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } catch (error) { - softError( - new ErrorWithFields({ - message: 'useLocalStorageNoSync setValue call failed', - source: error, - fields: { - key, - }, - }) - ); - } - }; - - return [storedValue, setValue]; -}