From 1e0d077f2a26e926b7df0ae41a17d2c7101f28d7 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Mon, 8 Jan 2024 00:01:49 +0800 Subject: [PATCH] feat: add mobile nav menu support --- src/client/App.tsx | 9 +- src/client/components/MobileNavItem.tsx | 28 +++++ src/client/hooks/useIsMobile.ts | 7 ++ src/client/hooks/useWindowSize.ts | 31 +++++ src/client/pages/Layout.tsx | 161 +++++++++++++++++------- 5 files changed, 187 insertions(+), 49 deletions(-) create mode 100644 src/client/components/MobileNavItem.tsx create mode 100644 src/client/hooks/useIsMobile.ts create mode 100644 src/client/hooks/useWindowSize.ts diff --git a/src/client/App.tsx b/src/client/App.tsx index bbf955d..a42aec5 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -9,7 +9,7 @@ import { Register } from './pages/Register'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from './api/cache'; import { TokenLoginContainer } from './components/TokenLoginContainer'; -import React, { Suspense } from 'react'; +import React, { Suspense, useRef } from 'react'; import { trpc, trpcClient } from './api/trpc'; import { MonitorPage } from './pages/Monitor'; import { WebsitePage } from './pages/Website'; @@ -58,12 +58,14 @@ export const AppRoutes: React.FC = React.memo(() => { AppRoutes.displayName = 'AppRoutes'; export const App: React.FC = React.memo(() => { + const rootRef = useRef(null); const colorScheme = useSettingsStore((state) => state.colorScheme); const algorithm = colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm; return (
{ - + rootRef.current!} + > diff --git a/src/client/components/MobileNavItem.tsx b/src/client/components/MobileNavItem.tsx new file mode 100644 index 0000000..c1a2ef4 --- /dev/null +++ b/src/client/components/MobileNavItem.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import clsx from 'clsx'; + +export const MobileNavItem: React.FC<{ + to: string; + label: string; + onClick?: () => void; +}> = React.memo((props) => { + const location = useLocation(); + + const isCurrent = location.pathname.startsWith(props.to); + + return ( + +
+ {props.label} +
+ + ); +}); +MobileNavItem.displayName = 'MobileNavItem'; diff --git a/src/client/hooks/useIsMobile.ts b/src/client/hooks/useIsMobile.ts new file mode 100644 index 0000000..6575bb4 --- /dev/null +++ b/src/client/hooks/useIsMobile.ts @@ -0,0 +1,7 @@ +import { useWindowSize } from './useWindowSize'; + +export function useIsMobile(): boolean { + const { width } = useWindowSize(); + + return width <= 768; +} diff --git a/src/client/hooks/useWindowSize.ts b/src/client/hooks/useWindowSize.ts new file mode 100644 index 0000000..b1d90e0 --- /dev/null +++ b/src/client/hooks/useWindowSize.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; + +interface WindowScreenSize { + width: number; + height: number; +} + +function getWindowSize(): WindowScreenSize { + return { + width: window.screen.width, + height: window.screen.height, + }; +} + +export function useWindowSize(): WindowScreenSize { + const [size, setSize] = useState(getWindowSize()); + + useEffect(() => { + const handleResize = () => { + setSize(getWindowSize()); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return size; +} diff --git a/src/client/pages/Layout.tsx b/src/client/pages/Layout.tsx index 63aebcf..ad5dfdf 100644 --- a/src/client/pages/Layout.tsx +++ b/src/client/pages/Layout.tsx @@ -1,12 +1,15 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Outlet, useSearchParams } from 'react-router-dom'; import { NavItem } from '../components/NavItem'; +import { MobileNavItem } from '../components/MobileNavItem'; import { UserOutlined } from '@ant-design/icons'; -import { Button, Dropdown } from 'antd'; +import { Button, Divider, Drawer, Dropdown, Menu } from 'antd'; import { useUserStore } from '../store/user'; import { useLogout } from '../api/model/user'; import { ColorSchemeSwitcher } from '../components/ColorSchemeSwitcher'; import { version } from '../../shared'; +import { useIsMobile } from '../hooks/useIsMobile'; +import { RiMenuUnfoldLine } from 'react-icons/ri'; export const Layout: React.FC = React.memo(() => { const [params] = useSearchParams(); @@ -23,64 +26,128 @@ export const Layout: React.FC = React.memo(() => { return []; }); + const [openDraw, setOpenDraw] = useState(false); const logout = useLogout(); + const isMobile = useIsMobile(); const showHeader = !params.has('hideHeader'); + const accountEl = ( + ({ + key: w.id, + label: `${w.name}${w.current ? '(current)' : ''}`, + disabled: w.current, + })), + }, + { + key: 'logout', + label: 'Logout', + onClick: () => { + logout(); + }, + }, + { + type: 'divider', + }, + { + key: 'version', + label: `v${version}`, + disabled: true, + }, + ], + }} + > +
+
+ + + {accountEl} +
+ + )} )}