Use dropdown nav

This commit is contained in:
LittleSheep 2024-02-26 21:13:47 +08:00
parent 14e87d96ce
commit 14efa09486
30 changed files with 120 additions and 217 deletions

View File

@ -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.

View File

@ -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

Binary file not shown.

View File

@ -1,4 +1,4 @@
package view
package views
import "embed"

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View 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)} />
</>
);
}

View File

@ -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>
);
}