✨ Use dropdown nav
This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"code.smartsheep.studio/hydrogen/identity/pkg/view"
 | 
			
		||||
	"code.smartsheep.studio/hydrogen/identity/pkg/views"
 | 
			
		||||
	"github.com/gofiber/fiber/v2"
 | 
			
		||||
	"github.com/gofiber/fiber/v2/middleware/cache"
 | 
			
		||||
	"github.com/gofiber/fiber/v2/middleware/cors"
 | 
			
		||||
@@ -92,7 +92,7 @@ func NewServer() {
 | 
			
		||||
		Expiration:   24 * time.Hour,
 | 
			
		||||
		CacheControl: true,
 | 
			
		||||
	}), filesystem.New(filesystem.Config{
 | 
			
		||||
		Root:         http.FS(view.FS),
 | 
			
		||||
		Root:         http.FS(views.FS),
 | 
			
		||||
		PathPrefix:   "dist",
 | 
			
		||||
		Index:        "index.html",
 | 
			
		||||
		NotFoundFile: "dist/index.html",
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -1,136 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  Slide,
 | 
			
		||||
  Toolbar,
 | 
			
		||||
  Typography,
 | 
			
		||||
  AppBar as MuiAppBar,
 | 
			
		||||
  AppBarProps as MuiAppBarProps,
 | 
			
		||||
  useScrollTrigger,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  styled,
 | 
			
		||||
  Box,
 | 
			
		||||
  useMediaQuery,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { ReactElement, ReactNode, useEffect, useState } from "react";
 | 
			
		||||
import { SITE_NAME } from "@/consts";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
import NavigationDrawer, { DRAWER_WIDTH, AppNavigationHeader, isMobileQuery } from "@/components/NavigationDrawer";
 | 
			
		||||
import MenuIcon from "@mui/icons-material/Menu";
 | 
			
		||||
 | 
			
		||||
function HideOnScroll(props: { window?: () => Window; children: ReactElement }) {
 | 
			
		||||
  const { children, window } = props;
 | 
			
		||||
  const trigger = useScrollTrigger({
 | 
			
		||||
    target: window ? window() : undefined,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Slide appear={false} direction="down" in={!trigger}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </Slide>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AppBarProps extends MuiAppBarProps {
 | 
			
		||||
  open?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ShellAppBar = styled(MuiAppBar, {
 | 
			
		||||
  shouldForwardProp: (prop) => prop !== "open",
 | 
			
		||||
})<AppBarProps>(({ theme, open }) => {
 | 
			
		||||
  const isMobile = useMediaQuery(isMobileQuery);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    transition: theme.transitions.create(["margin", "width"], {
 | 
			
		||||
      easing: theme.transitions.easing.sharp,
 | 
			
		||||
      duration: theme.transitions.duration.leavingScreen,
 | 
			
		||||
    }),
 | 
			
		||||
    ...(!isMobile &&
 | 
			
		||||
      open && {
 | 
			
		||||
        width: `calc(100% - ${DRAWER_WIDTH}px)`,
 | 
			
		||||
        transition: theme.transitions.create(["margin", "width"], {
 | 
			
		||||
          easing: theme.transitions.easing.easeOut,
 | 
			
		||||
          duration: theme.transitions.duration.enteringScreen,
 | 
			
		||||
        }),
 | 
			
		||||
        marginRight: DRAWER_WIDTH,
 | 
			
		||||
      }),
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{
 | 
			
		||||
  open?: boolean;
 | 
			
		||||
}>(({ theme, open }) => {
 | 
			
		||||
  const isMobile = useMediaQuery(isMobileQuery);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    flexGrow: 1,
 | 
			
		||||
    transition: theme.transitions.create("margin", {
 | 
			
		||||
      easing: theme.transitions.easing.sharp,
 | 
			
		||||
      duration: theme.transitions.duration.leavingScreen,
 | 
			
		||||
    }),
 | 
			
		||||
    marginRight: -DRAWER_WIDTH,
 | 
			
		||||
    ...(!isMobile &&
 | 
			
		||||
      open && {
 | 
			
		||||
        transition: theme.transitions.create("margin", {
 | 
			
		||||
          easing: theme.transitions.easing.easeOut,
 | 
			
		||||
          duration: theme.transitions.duration.enteringScreen,
 | 
			
		||||
        }),
 | 
			
		||||
        marginRight: 0,
 | 
			
		||||
      }),
 | 
			
		||||
    position: "relative",
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default function AppShell({ children }: { children: ReactNode }) {
 | 
			
		||||
  let documentWindow: Window;
 | 
			
		||||
 | 
			
		||||
  const isMobile = useMediaQuery(isMobileQuery);
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    documentWindow = window;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <HideOnScroll window={() => documentWindow}>
 | 
			
		||||
        <ShellAppBar open={open} position="fixed">
 | 
			
		||||
          <Toolbar sx={{ height: 64 }}>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              size="large"
 | 
			
		||||
              edge="start"
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              aria-label="menu"
 | 
			
		||||
              sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }}
 | 
			
		||||
            >
 | 
			
		||||
              <img src="/favicon.svg" alt="Logo" width={32} height={32} />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
 | 
			
		||||
            <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}>
 | 
			
		||||
              <Link to="/">{SITE_NAME}</Link>
 | 
			
		||||
            </Typography>
 | 
			
		||||
 | 
			
		||||
            <IconButton
 | 
			
		||||
              size="large"
 | 
			
		||||
              edge="start"
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              aria-label="menu"
 | 
			
		||||
              onClick={() => setOpen(true)}
 | 
			
		||||
              sx={{ width: 64, mr: 1, display: !isMobile && open ? "none" : "block" }}
 | 
			
		||||
            >
 | 
			
		||||
              <MenuIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Toolbar>
 | 
			
		||||
        </ShellAppBar>
 | 
			
		||||
      </HideOnScroll>
 | 
			
		||||
 | 
			
		||||
      <Box sx={{ display: "flex" }}>
 | 
			
		||||
        <AppMain open={open}>
 | 
			
		||||
          <AppNavigationHeader />
 | 
			
		||||
 | 
			
		||||
          {children}
 | 
			
		||||
        </AppMain>
 | 
			
		||||
 | 
			
		||||
        <NavigationDrawer open={open} onClose={() => setOpen(false)} />
 | 
			
		||||
      </Box>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								pkg/views/bun.lockb
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								pkg/views/bun.lockb
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
package view
 | 
			
		||||
package views
 | 
			
		||||
 | 
			
		||||
import "embed"
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										95
									
								
								pkg/views/src/components/AppShell.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								pkg/views/src/components/AppShell.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
import {
 | 
			
		||||
  AppBar,
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Slide,
 | 
			
		||||
  Toolbar,
 | 
			
		||||
  Typography,
 | 
			
		||||
  useMediaQuery,
 | 
			
		||||
  useScrollTrigger
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { ReactElement, ReactNode, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { SITE_NAME } from "@/consts";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
import NavigationMenu, { AppNavigationHeader, isMobileQuery } from "@/components/NavigationMenu.tsx";
 | 
			
		||||
import AccountCircleIcon from "@mui/icons-material/AccountCircleOutlined";
 | 
			
		||||
import { useUserinfo } from "@/stores/userinfo.tsx";
 | 
			
		||||
 | 
			
		||||
function HideOnScroll(props: { window?: () => Window; children: ReactElement }) {
 | 
			
		||||
  const { children, window } = props;
 | 
			
		||||
  const trigger = useScrollTrigger({
 | 
			
		||||
    target: window ? window() : undefined
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Slide appear={false} direction="down" in={!trigger}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </Slide>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function AppShell({ children }: { children: ReactNode }) {
 | 
			
		||||
  let documentWindow: Window;
 | 
			
		||||
 | 
			
		||||
  const { userinfo } = useUserinfo();
 | 
			
		||||
 | 
			
		||||
  const isMobile = useMediaQuery(isMobileQuery);
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    documentWindow = window;
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const container = useRef<HTMLDivElement>(null);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <HideOnScroll window={() => documentWindow}>
 | 
			
		||||
        <AppBar position="fixed">
 | 
			
		||||
          <Toolbar sx={{ height: 64 }}>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              size="large"
 | 
			
		||||
              edge="start"
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              aria-label="menu"
 | 
			
		||||
              sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }}
 | 
			
		||||
            >
 | 
			
		||||
              <img src="/favicon.svg" alt="Logo" width={32} height={32} />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
 | 
			
		||||
            <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}>
 | 
			
		||||
              <Link to="/">{SITE_NAME}</Link>
 | 
			
		||||
            </Typography>
 | 
			
		||||
 | 
			
		||||
            <IconButton
 | 
			
		||||
              size="large"
 | 
			
		||||
              edge="start"
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              aria-label="menu"
 | 
			
		||||
              onClick={() => setOpen(true)}
 | 
			
		||||
              sx={{ mr: 1 }}
 | 
			
		||||
            >
 | 
			
		||||
              <Avatar
 | 
			
		||||
                sx={{ width: 32, height: 32, bgcolor: "transparent" }}
 | 
			
		||||
                ref={container}
 | 
			
		||||
                alt={userinfo?.displayName}
 | 
			
		||||
                src={userinfo?.profiles?.avatar}
 | 
			
		||||
              >
 | 
			
		||||
                <AccountCircleIcon />
 | 
			
		||||
              </Avatar>
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Toolbar>
 | 
			
		||||
        </AppBar>
 | 
			
		||||
      </HideOnScroll>
 | 
			
		||||
 | 
			
		||||
      <Box component="main">
 | 
			
		||||
        <AppNavigationHeader />
 | 
			
		||||
 | 
			
		||||
        {children}
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
      <NavigationMenu anchorEl={container.current} open={open} onClose={() => setOpen(false)} />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +1,15 @@
 | 
			
		||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
 | 
			
		||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Collapse,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Drawer,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItemButton,
 | 
			
		||||
  ListItemIcon,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  styled,
 | 
			
		||||
  useMediaQuery
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { Collapse, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled } from "@mui/material";
 | 
			
		||||
import { theme } from "@/theme";
 | 
			
		||||
import { Fragment, ReactNode, useState } from "react";
 | 
			
		||||
import HowToRegIcon from "@mui/icons-material/HowToReg";
 | 
			
		||||
import LoginIcon from "@mui/icons-material/Login";
 | 
			
		||||
import FaceIcon from "@mui/icons-material/Face";
 | 
			
		||||
import LogoutIcon from "@mui/icons-material/Logout";
 | 
			
		||||
import LogoutIcon from "@mui/icons-material/ExitToApp";
 | 
			
		||||
import ExpandLess from "@mui/icons-material/ExpandLess";
 | 
			
		||||
import ExpandMore from "@mui/icons-material/ExpandMore";
 | 
			
		||||
import { useUserinfo } from "@/stores/userinfo.tsx";
 | 
			
		||||
import { PopoverProps } from "@mui/material/Popover";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export interface NavigationItem {
 | 
			
		||||
  icon?: ReactNode;
 | 
			
		||||
@@ -51,32 +39,30 @@ export function AppNavigationSection({ items, depth }: { items: NavigationItem[]
 | 
			
		||||
    } else if (item.children) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Fragment key={idx}>
 | 
			
		||||
          <ListItemButton onClick={() => setOpen(!open)} sx={{ pl: 2 + (depth ?? 0) * 2 }}>
 | 
			
		||||
          <MenuItem onClick={() => setOpen(!open)} sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}>
 | 
			
		||||
            <ListItemIcon>{item.icon}</ListItemIcon>
 | 
			
		||||
            <ListItemText primary={item.title} />
 | 
			
		||||
            {open ? <ExpandLess /> : <ExpandMore />}
 | 
			
		||||
          </ListItemButton>
 | 
			
		||||
          </MenuItem>
 | 
			
		||||
          <Collapse in={open} timeout="auto" unmountOnExit>
 | 
			
		||||
            <List component="div" disablePadding>
 | 
			
		||||
              <AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} />
 | 
			
		||||
            </List>
 | 
			
		||||
            <AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} />
 | 
			
		||||
          </Collapse>
 | 
			
		||||
        </Fragment>
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return (
 | 
			
		||||
        <a key={idx} href={item.link ?? "/"}>
 | 
			
		||||
          <ListItemButton sx={{ pl: 2 + (depth ?? 0) * 2 }}>
 | 
			
		||||
        <Link key={idx} to={item.link ?? "/"}>
 | 
			
		||||
          <MenuItem sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}>
 | 
			
		||||
            <ListItemIcon>{item.icon}</ListItemIcon>
 | 
			
		||||
            <ListItemText primary={item.title} />
 | 
			
		||||
          </ListItemButton>
 | 
			
		||||
        </a>
 | 
			
		||||
          </MenuItem>
 | 
			
		||||
        </Link>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) {
 | 
			
		||||
export function AppNavigation() {
 | 
			
		||||
  const { checkLoggedIn } = useUserinfo();
 | 
			
		||||
 | 
			
		||||
  const nav: NavigationItem[] = [
 | 
			
		||||
@@ -94,61 +80,19 @@ export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onC
 | 
			
		||||
    )
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AppNavigationHeader>
 | 
			
		||||
        {showClose && (
 | 
			
		||||
          <IconButton onClick={onClose}>
 | 
			
		||||
            {theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />}
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        )}
 | 
			
		||||
      </AppNavigationHeader>
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <List>
 | 
			
		||||
        <AppNavigationSection items={nav} />
 | 
			
		||||
      </List>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <AppNavigationSection items={nav} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const isMobileQuery = theme.breakpoints.down("md");
 | 
			
		||||
 | 
			
		||||
export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
 | 
			
		||||
  const isMobile = useMediaQuery(isMobileQuery);
 | 
			
		||||
 | 
			
		||||
  return isMobile ? (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box sx={{ flexShrink: 0, width: DRAWER_WIDTH }} />
 | 
			
		||||
      <Drawer
 | 
			
		||||
        keepMounted
 | 
			
		||||
        anchor="right"
 | 
			
		||||
        variant="temporary"
 | 
			
		||||
        open={open}
 | 
			
		||||
        onClose={onClose}
 | 
			
		||||
        sx={{
 | 
			
		||||
          "& .MuiDrawer-paper": {
 | 
			
		||||
            boxSizing: "border-box",
 | 
			
		||||
            width: DRAWER_WIDTH
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <AppNavigation onClose={onClose} />
 | 
			
		||||
      </Drawer>
 | 
			
		||||
    </>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <Drawer
 | 
			
		||||
      variant="persistent"
 | 
			
		||||
      anchor="right"
 | 
			
		||||
      open={open}
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: DRAWER_WIDTH,
 | 
			
		||||
        flexShrink: 0,
 | 
			
		||||
        "& .MuiDrawer-paper": {
 | 
			
		||||
          width: DRAWER_WIDTH
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <AppNavigation showClose onClose={onClose} />
 | 
			
		||||
    </Drawer>
 | 
			
		||||
export default function NavigationMenu({ anchorEl, open, onClose }: {
 | 
			
		||||
  anchorEl: PopoverProps["anchorEl"];
 | 
			
		||||
  open: boolean;
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Menu anchorEl={anchorEl} open={open} onClose={onClose}>
 | 
			
		||||
      <AppNavigation />
 | 
			
		||||
    </Menu>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user