diff --git a/web/src/App.jsx b/web/src/App.jsx index e7e642ac4..eb55bafde 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -11,13 +11,13 @@ import { Router } from 'preact-router'; import Sidebar from './Sidebar'; import StyleGuide from './StyleGuide'; import Api, { FetchStatus, useConfig } from './api'; -import { DarkModeProvider, SidebarProvider } from './context'; +import { DarkModeProvider, DrawerProvider } from './context'; export default function App() { const { data, status } = useConfig(); return ( - +
{status !== FetchStatus.LOADED ? ( @@ -41,7 +41,7 @@ export default function App() {
)} -
+
); } diff --git a/web/src/Sidebar.jsx b/web/src/Sidebar.jsx index 6821a946c..94aceec30 100644 --- a/web/src/Sidebar.jsx +++ b/web/src/Sidebar.jsx @@ -1,56 +1,45 @@ import { h, Fragment } from 'preact'; -import Link from './components/Link'; import LinkedLogo from './components/LinkedLogo'; -import { Link as RouterLink } from 'preact-router/match'; -import { useCallback, useState } from 'preact/hooks'; -import { useSidebar } from './context'; - -function NavLink({ className = '', href, text, ...other }) { - const external = href.startsWith('http'); - const El = external ? Link : RouterLink; - const props = external ? { rel: 'noopener nofollow', target: '_blank' } : {}; - return ( - - {text} - - ); -} +import { Match } from 'preact-router/match'; +import { memo } from 'preact/compat'; +import { useConfig } from './api'; +import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer'; +import { useCallback, useMemo } from 'preact/hooks'; export default function Sidebar() { - const { showSidebar, setShowSidebar } = useSidebar(); - - const handleDismiss = useCallback(() => { - setShowSidebar(false); - }, [setShowSidebar]); + const { data: config } = useConfig(); + const cameras = useMemo(() => Object.keys(config.cameras), [config]); return ( - - {showSidebar ?
: ''} -
-
-
- -
-
- -
- + }> + + + {({ matches }) => + matches ? ( + + + {cameras.map((camera) => ( + + ))} + + + ) : null + } + + + + +
+ + + ); } + +const Header = memo(function Header() { + return ( +
+ +
+ ); +}); diff --git a/web/src/components/AppBar.jsx b/web/src/components/AppBar.jsx index 0238e3f95..089b1b508 100644 --- a/web/src/components/AppBar.jsx +++ b/web/src/components/AppBar.jsx @@ -7,7 +7,7 @@ import MoreIcon from '../icons/More'; import AutoAwesomeIcon from '../icons/AutoAwesome'; import LightModeIcon from '../icons/LightMode'; import DarkModeIcon from '../icons/DarkMode'; -import { useDarkMode, useSidebar } from '../context'; +import { useDarkMode, useDrawer } from '../context'; import { useLayoutEffect, useCallback, useRef, useState } from 'preact/hooks'; // We would typically preserve these in component state @@ -18,10 +18,10 @@ let lastScrollY = window.scrollY; export default function AppBar({ title }) { const [show, setShow] = useState(true); const [atZero, setAtZero] = useState(window.scrollY === 0); - const [sidebarVisible, setSidebarVisible] = useState(true); + const [_, setDrawerVisible] = useState(true); const [showMoreMenu, setShowMoreMenu] = useState(false); const { currentMode, persistedMode, setDarkMode } = useDarkMode(); - const { showSidebar, setShowSidebar } = useSidebar(); + const { showDrawer, setShowDrawer } = useDrawer(); const handleSelectDarkMode = useCallback( (value, label) => { @@ -65,9 +65,9 @@ export default function AppBar({ title }) { setShowMoreMenu(false); }, [setShowMoreMenu]); - const handleShowSidebar = useCallback(() => { - setShowSidebar(true); - }, [setShowSidebar]); + const handleShowDrawer = useCallback(() => { + setShowDrawer(true); + }, [setShowDrawer]); return (
-
diff --git a/web/src/components/Link.jsx b/web/src/components/Link.jsx index dbe0518b0..3547996d7 100644 --- a/web/src/components/Link.jsx +++ b/web/src/components/Link.jsx @@ -1,9 +1,16 @@ import { h } from 'preact'; +import { Link as RouterLink } from 'preact-router/match'; -export default function Link({ className, children, href, ...props }) { +export default function Link({ + activeClassName = '', + className = 'text-blue-500 hover:underline', + children, + href, + ...props +}) { return ( - + {children} - + ); } diff --git a/web/src/components/Menu.jsx b/web/src/components/Menu.jsx index 831b6c920..dc521a44e 100644 --- a/web/src/components/Menu.jsx +++ b/web/src/components/Menu.jsx @@ -6,7 +6,7 @@ export default function Menu({ className, children, onDismiss, relativeTo }) { return relativeTo ? ( ; + return
; } diff --git a/web/src/components/NavigationDrawer.jsx b/web/src/components/NavigationDrawer.jsx new file mode 100644 index 000000000..fc6d08d6e --- /dev/null +++ b/web/src/components/NavigationDrawer.jsx @@ -0,0 +1,61 @@ +import { h, Fragment } from 'preact'; +import { Link } from 'preact-router/match'; +import { useCallback, useState } from 'preact/hooks'; +import { useDrawer } from '../context'; + +export default function NavigationDrawer({ children, header }) { + const { showDrawer, setShowDrawer } = useDrawer(); + + const handleDismiss = useCallback(() => { + setShowDrawer(false); + }, [setShowDrawer]); + + return ( + + {showDrawer ?
: ''} +
+ {header ? ( +
+ {header} +
+ ) : null} + + +
+ + ); +} + +export function Destination({ className = '', href, text, ...other }) { + const external = href.startsWith('http'); + const props = external ? { rel: 'noopener nofollow', target: '_blank' } : {}; + + const { setShowDrawer } = useDrawer(); + + const handleDismiss = useCallback(() => { + setTimeout(() => { + setShowDrawer(false); + }, 250); + }, [setShowDrawer]); + + const styleProps = { + [external + ? 'className' + : 'class']: 'block p-2 text-sm font-semibold text-gray-900 rounded hover:bg-blue-500 dark:text-gray-200 hover:text-white dark:hover:text-white focus:outline-none ring-opacity-50 focus:ring-2 ring-blue-300', + }; + + return ( + +
{text}
+ + ); +} + +export function Separator() { + return
; +} diff --git a/web/src/components/RelativeModal.jsx b/web/src/components/RelativeModal.jsx index 705db416b..d6b031226 100644 --- a/web/src/components/RelativeModal.jsx +++ b/web/src/components/RelativeModal.jsx @@ -1,5 +1,6 @@ import { h, Fragment } from 'preact'; import { createPortal } from 'preact/compat'; +import { DarkModeProvider } from '../context'; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; const WINDOW_PADDING = 20; @@ -75,7 +76,7 @@ export default function RelativeModal({ className, role = 'dialog', children, on }, [show, position.width, ref.current]); const menu = ( - +
{children}
- + ); return portalRoot ? createPortal(menu, portalRoot) : menu; diff --git a/web/src/context/index.jsx b/web/src/context/index.jsx index 06ae4dfaf..40ff9f560 100644 --- a/web/src/context/index.jsx +++ b/web/src/context/index.jsx @@ -65,14 +65,14 @@ export function useDarkMode() { return useContext(DarkMode); } -const Sidebar = createContext(null); +const Drawer = createContext(null); -export function SidebarProvider({ children }) { - const [showSidebar, setShowSidebar] = useState(false); +export function DrawerProvider({ children }) { + const [showDrawer, setShowDrawer] = useState(false); - return {children}; + return {children}; } -export function useSidebar() { - return useContext(Sidebar); +export function useDrawer() { + return useContext(Drawer); }