diff --git a/public/sw.js b/public/sw.js index 9fdea8d..2e3a5a5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -49,12 +49,12 @@ async function networkFirst(request) { const cacheRes = shouldCache(request, response); if (cacheRes) { - cache.put(cacheRes === "home" ? "/" : request, response.clone()); + cache.put(cacheRes === "home" ? "/home" : request, response.clone()); } return response ?? await cache.match(request); } catch (error) { - return await cache.match(request).then(async res => res ?? await cache.match("/")); + return await cache.match(request).then(async res => res ?? await cache.match("/home")); } } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7b9d423..c3592c3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import { ToastContainer as MessageToast } from "react-toastify"; import HomePage from "./pages/HomePage"; import LoadingPage from "./pages/LoadingPage"; +import NavigationSelector from "./pages/NavigationSelector"; import TimetablePage from "./pages/TimetablePage"; import { Status } from "./types/utils"; import { RECEIVED_DONATION_NOTIFICATION, TOAST_AUTO_CLOSE_TIME } from "./utils/constants"; @@ -43,7 +44,8 @@ const App = () => { <> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/context/datalistFocus.tsx b/src/context/datalistFocus.tsx index 904d320..e357bac 100644 --- a/src/context/datalistFocus.tsx +++ b/src/context/datalistFocus.tsx @@ -1,6 +1,4 @@ import useInputFocus from "@/hooks/useFocus"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; -import { TABLET_SCREEN_BREAKPOINT } from "@/utils/constants"; import type React from "react"; import { type ReactNode, createContext, useContext } from "react"; @@ -14,10 +12,8 @@ interface DatalistFocusContextType { const DatalistFocusContext = createContext(undefined); const DatalistFocusProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const { width } = useWindowDimensions(); const { ref, isFocused, focus, blur } = useInputFocus({ childInput: true, - initFocus: width > TABLET_SCREEN_BREAKPOINT, }); return ( diff --git a/src/features/header/HomeHeader.tsx b/src/features/header/HomeHeader.tsx index e6dccc7..856335b 100644 --- a/src/features/header/HomeHeader.tsx +++ b/src/features/header/HomeHeader.tsx @@ -20,7 +20,7 @@ const HeaderPanel: FC = ({ timetableType, className }) => { () => width < TABLET_SCREEN_BREAKPOINT && width > NARROW_SCREEN_BREAKPOINT, [width] ); - const { isFocused, blur, focus } = useDatalistFocus(); + const { isFocused, blur } = useDatalistFocus(); const [showSearchBar, setShowSearchBar] = useState(!shouldShrinkSearchBar || isFocused); useEffect(() => { @@ -40,10 +40,8 @@ const HeaderPanel: FC = ({ timetableType, className }) => { useEffect(() => { if (!showSearchBar) { blur(); - } else { - focus(); } - }, [showSearchBar, blur, focus]); + }, [showSearchBar, blur]); const toggleSearchBar = (state = !showSearchBar) => { if (shouldShrinkSearchBar) { diff --git a/src/features/header/TimetableHeader.tsx b/src/features/header/TimetableHeader.tsx index 33934fb..ca7649e 100644 --- a/src/features/header/TimetableHeader.tsx +++ b/src/features/header/TimetableHeader.tsx @@ -1,10 +1,9 @@ import HomeIcon from "@/assets/HomeIcon"; import usePageTitle from "@/hooks/usePageTitle"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; +import { useIsMobile } from "@/hooks/useWindowDimensions"; import Toggle from "@/shared/Toggle"; import { classes } from "@/styles/utils"; import type { HalfTerm } from "@/types/timetable"; -import { MOBILE_SCREEN_BREAKPOINT } from "@/utils/constants"; import TimetableManager from "@/utils/data/TimetableManager"; import { isMerged } from "@/utils/timetable"; import Toast from "@/utils/toasts"; @@ -41,8 +40,7 @@ const TimetableHeader: FC = ({ const [isSecondWeek, setIsSecondWeek] = weekState; const navigate = useNavigate(); const group = useParams().group?.trim() ?? ""; - const { width } = useWindowDimensions(); - const isMobile = width < MOBILE_SCREEN_BREAKPOINT; + const isMobile = useIsMobile(); const groupTitle = timetableType === "merged" ? "Мій розклад" : group; usePageTitle(groupTitle); @@ -70,7 +68,7 @@ const TimetableHeader: FC = ({
= ({ timetableType }) => { - const { width } = useWindowDimensions(); - const isMobile = width < TABLET_SCREEN_BREAKPOINT; + const isMobile = useIsMobile(); const navigate = useNavigate(); const onMobileSelectChange = (type: string) => { @@ -32,7 +30,7 @@ const Navigation: FC = ({ timetableType }) => { navigationItems.map((type) => ( diff --git a/src/features/home/TimetablesSelection.tsx b/src/features/home/TimetablesSelection.tsx index 8cfc7fa..7714c55 100644 --- a/src/features/home/TimetablesSelection.tsx +++ b/src/features/home/TimetablesSelection.tsx @@ -1,4 +1,5 @@ import { useDatalistFocus } from "@/context/datalistFocus"; +import { useIsMobile } from "@/hooks/useWindowDimensions"; import { classes } from "@/styles/utils"; import { sortGroupsByYear } from "@/utils/timetable"; import { type FC, useState } from "react"; @@ -20,6 +21,7 @@ type OwnProps = { const TimetablesSelection: FC = ({ timetables, withYears = false }) => { const groupsByYear = sortGroupsByYear(timetables); const [expandedYear, setExpandedYear] = useState(null); // for mobile onClick event and keyboard navigation + const isMobile = useIsMobile(); const { focus } = useDatalistFocus(); return ( @@ -30,9 +32,10 @@ const TimetablesSelection: FC = ({ timetables, withYears = false }) => groupsByYear[year]?.length && groupsByYear[year]?.length !== 0 ? (
    { + if (isMobile) return; expandedYear === year ? setExpandedYear(null) : setExpandedYear(year); }} > diff --git a/src/features/timetable/Timetable.tsx b/src/features/timetable/Timetable.tsx index d6cb781..096442e 100644 --- a/src/features/timetable/Timetable.tsx +++ b/src/features/timetable/Timetable.tsx @@ -1,7 +1,7 @@ -import useWindowDimensions from "@/hooks/useWindowDimensions"; +import { useIsMobile } from "@/hooks/useWindowDimensions"; import { classes } from "@/styles/utils"; import type { TimetableItem } from "@/types/timetable"; -import { DEVELOP, TIMETABLE_SCREEN_BREAKPOINT } from "@/utils/constants"; +import { DEVELOP } from "@/utils/constants"; import { getCurrentUADate, stringToDate } from "@/utils/date"; import { generateSaturdayLessons, lessonsTimes, skeletonTimetable, unique } from "@/utils/timetable"; import { type FC, useCallback, useEffect, useMemo, useState } from "react"; @@ -28,8 +28,7 @@ const Timetable: FC = ({ hasCellSubgroups, isLoading, }) => { - const { width } = useWindowDimensions(); - const isMobile = width < TIMETABLE_SCREEN_BREAKPOINT; + const isMobile = useIsMobile(); const timetable = useMemo(() => { return [...originalTimetable, ...generateSaturdayLessons(originalTimetable)]; diff --git a/src/hooks/useWindowDimensions.ts b/src/hooks/useWindowDimensions.ts index 1dc1126..8bfef5c 100644 --- a/src/hooks/useWindowDimensions.ts +++ b/src/hooks/useWindowDimensions.ts @@ -1,3 +1,4 @@ +import { MOBILE_SCREEN_BREAKPOINT, TABLET_SCREEN_BREAKPOINT } from "@/utils/constants"; import { useEffect, useState } from "react"; function getWindowDimensions() { @@ -24,3 +25,13 @@ export default function useWindowDimensions() { return windowDimensions; } + +export function useIsMobile() { + const { width } = useWindowDimensions(); + return width < MOBILE_SCREEN_BREAKPOINT; +} + +export function useIsTablet() { + const { width } = useWindowDimensions(); + return width < TABLET_SCREEN_BREAKPOINT; +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index edb46f5..79e1e2f 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -4,15 +4,15 @@ import catImage from "@/assets/cat.svg"; import { DatalistFocusProvider } from "@/context/datalistFocus"; import HeaderPanel from "@/features/header/HomeHeader"; import TimetablesSelection from "@/features/home/TimetablesSelection"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; +import { useIsTablet } from "@/hooks/useWindowDimensions"; import List from "@/shared/List"; import { classes } from "@/styles/utils"; import type { TimetableType } from "@/types/timetable"; -import { BUG_REPORT_LINK, DONATION_LINK, TABLET_SCREEN_BREAKPOINT } from "@/utils/constants"; +import { BUG_REPORT_LINK, DONATION_LINK } from "@/utils/constants"; import TimetableManager from "@/utils/data/TimetableManager"; import Toast from "@/utils/toasts"; -import { type FC, useCallback, useEffect, useState } from "react"; -import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; +import { type FC, useCallback, useEffect, useRef, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import styles from "./HomePage.module.scss"; type OwnProps = { @@ -26,44 +26,45 @@ const HomePage: FC = ({ timetableType }) => { const [selectedFirst, setSelectedFirst] = useState(null); const [selectedSecond, setSelectedSecond] = useState(null); - const { state }: { state: { force: boolean } | null } = useLocation(); - const { force } = state ?? {}; - - const { width } = useWindowDimensions(); - const navigate = useNavigate(); - const isTablet = width < TABLET_SCREEN_BREAKPOINT; + const isTablet = useIsTablet(); const showFirstLayer = !isTablet || !selectedSecond; const showSecondLayer = showFirstLayer && secondLayer.length > 0; const showThirdLayer = Boolean(selectedSecond); + const timetableTypeRef = useRef(timetableType); + + useEffect(() => { + timetableTypeRef.current = timetableType; + }, [timetableType]); + const [searchParams, setSearchParams] = useSearchParams(); - const updateSecondLayer = useCallback( - (query: string) => { - TimetableManager.updateLastOpenedInstitute(query); - Toast.promise(TimetableManager.getSecondLayerByType(timetableType, query), "Fetching groups...") - .then(setSecondLayer) - .catch(Toast.error); - }, - [timetableType] - ); + const updateSecondLayer = useCallback((query: string) => { + TimetableManager.updateLastOpenedInstitute(query); + return Toast.promise(TimetableManager.getSecondLayerByType(timetableTypeRef.current, query), "Fetching groups...") + .then(setSecondLayer) + .catch(Toast.error); + }, []); + + const updateThirdLayer = useCallback((major: string) => { + return Toast.promise( + TimetableManager.getThirdLayerByType(timetableTypeRef.current, major), + "Fetching timetables..." + ) + .then(setThirdLayer) + .catch(Toast.error); + }, []); - const updateThirdLayer = useCallback( - (major: string) => { - Toast.promise(TimetableManager.getThirdLayerByType(timetableType, major), "Fetching timetables...") - .then(setThirdLayer) - .catch(Toast.error); - }, - [timetableType] - ); + const reset = useCallback(() => { + setSelectedFirst(null); + setSelectedSecond(null); + setThirdLayer([]); + setSecondLayer([]); + }, []); // Initial fetch useEffect(() => { - if (!force && timetableType === "timetable" && searchParams.size === 0) { - TimetableManager.getLastOpenedTimetable().then((t) => { - t && navigate(t); - }); - } + reset(); Toast.promise(TimetableManager.getFirstLayerSelectionByType(timetableType), "Fetching institutes...") .then(setFirstLayer) @@ -73,11 +74,13 @@ const HomePage: FC = ({ timetableType }) => { return () => { Toast.hideAllMessages(); }; - }, [timetableType, force, navigate, searchParams]); + }, [timetableType, reset]); // On first layer change useEffect(() => { - if (!selectedFirst) return; + if (!selectedFirst) { + return; + } updateSecondLayer(selectedFirst); }, [selectedFirst, updateSecondLayer]); @@ -98,13 +101,18 @@ const HomePage: FC = ({ timetableType }) => { setSelectedSecond(null); setThirdLayer([]); } - if (secondLayer.includes(selectedMajor)) { + if (firstLayer.includes(selectedInstitute) && secondLayer.includes(selectedMajor)) { setSelectedSecond(selectedMajor); } + }, [firstLayer, secondLayer, searchParams]); + + useEffect(() => { + const selectedInstitute = searchParams.get("institute") || ""; + if (firstLayer.includes(selectedInstitute)) { setSelectedFirst(selectedInstitute); } - }, [secondLayer, searchParams, firstLayer]); + }, [firstLayer, searchParams]); const handleFirstChange = useCallback( (institute: string | null) => { diff --git a/src/pages/NavigationSelector.tsx b/src/pages/NavigationSelector.tsx new file mode 100644 index 0000000..eca005d --- /dev/null +++ b/src/pages/NavigationSelector.tsx @@ -0,0 +1,16 @@ +import TimetableManager from "@/utils/data/TimetableManager"; +import { useEffect } from "react"; +import { useNavigate } from "react-router"; + +const NavigationSelector = () => { + const navigate = useNavigate(); + + useEffect(() => { + TimetableManager.getLastOpenedTimetable().then((t) => { + navigate(t || "/home"); + }); + }, [navigate]); + return null; +}; + +export default NavigationSelector; diff --git a/src/pages/TimetablePage.tsx b/src/pages/TimetablePage.tsx index 48e1cca..3dabf7d 100644 --- a/src/pages/TimetablePage.tsx +++ b/src/pages/TimetablePage.tsx @@ -62,7 +62,7 @@ const TimetablePage: FC = ({ isExamsTimetable = false }) => { function onError(e: string, userError?: string) { Toast.error(e, userError); - navigate("/", { state: { force: true } }); + navigate("/home"); } // biome-ignore lint/correctness/useExhaustiveDependencies: I don't actually remember why but I don't want to break it diff --git a/src/utils/timetable.ts b/src/utils/timetable.ts index 8fd0b98..6ff93e5 100644 --- a/src/utils/timetable.ts +++ b/src/utils/timetable.ts @@ -198,6 +198,6 @@ export const skeletonTimetable = { export const pathnameToType = (pathname: string): TimetablePageType => { if (pathname.includes("lecturer")) return "lecturer"; if (pathname.includes("selective")) return "selective"; - if (pathname === "/") return "home"; + if (pathname.includes("home")) return "home"; return "timetable"; };