✨ Use dropdown nav
This commit is contained in:
parent
14e87d96ce
commit
14efa09486
@ -1,7 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
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"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
@ -92,7 +92,7 @@ func NewServer() {
|
|||||||
Expiration: 24 * time.Hour,
|
Expiration: 24 * time.Hour,
|
||||||
CacheControl: true,
|
CacheControl: true,
|
||||||
}), filesystem.New(filesystem.Config{
|
}), filesystem.New(filesystem.Config{
|
||||||
Root: http.FS(view.FS),
|
Root: http.FS(views.FS),
|
||||||
PathPrefix: "dist",
|
PathPrefix: "dist",
|
||||||
Index: "index.html",
|
Index: "index.html",
|
||||||
NotFoundFile: "dist/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"
|
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 { Collapse, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled } from "@mui/material";
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Collapse,
|
|
||||||
Divider,
|
|
||||||
Drawer,
|
|
||||||
IconButton,
|
|
||||||
List,
|
|
||||||
ListItemButton,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
styled,
|
|
||||||
useMediaQuery
|
|
||||||
} from "@mui/material";
|
|
||||||
import { theme } from "@/theme";
|
import { theme } from "@/theme";
|
||||||
import { Fragment, ReactNode, useState } from "react";
|
import { Fragment, ReactNode, useState } from "react";
|
||||||
import HowToRegIcon from "@mui/icons-material/HowToReg";
|
import HowToRegIcon from "@mui/icons-material/HowToReg";
|
||||||
import LoginIcon from "@mui/icons-material/Login";
|
import LoginIcon from "@mui/icons-material/Login";
|
||||||
import FaceIcon from "@mui/icons-material/Face";
|
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 ExpandLess from "@mui/icons-material/ExpandLess";
|
||||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||||
import { useUserinfo } from "@/stores/userinfo.tsx";
|
import { useUserinfo } from "@/stores/userinfo.tsx";
|
||||||
|
import { PopoverProps } from "@mui/material/Popover";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
@ -51,32 +39,30 @@ export function AppNavigationSection({ items, depth }: { items: NavigationItem[]
|
|||||||
} else if (item.children) {
|
} else if (item.children) {
|
||||||
return (
|
return (
|
||||||
<Fragment key={idx}>
|
<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>
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||||
<ListItemText primary={item.title} />
|
<ListItemText primary={item.title} />
|
||||||
{open ? <ExpandLess /> : <ExpandMore />}
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
</ListItemButton>
|
</MenuItem>
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
<List component="div" disablePadding>
|
<AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} />
|
||||||
<AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} />
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<a key={idx} href={item.link ?? "/"}>
|
<Link key={idx} to={item.link ?? "/"}>
|
||||||
<ListItemButton sx={{ pl: 2 + (depth ?? 0) * 2 }}>
|
<MenuItem sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}>
|
||||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||||
<ListItemText primary={item.title} />
|
<ListItemText primary={item.title} />
|
||||||
</ListItemButton>
|
</MenuItem>
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) {
|
export function AppNavigation() {
|
||||||
const { checkLoggedIn } = useUserinfo();
|
const { checkLoggedIn } = useUserinfo();
|
||||||
|
|
||||||
const nav: NavigationItem[] = [
|
const nav: NavigationItem[] = [
|
||||||
@ -94,61 +80,19 @@ export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onC
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return <AppNavigationSection items={nav} />;
|
||||||
<>
|
|
||||||
<AppNavigationHeader>
|
|
||||||
{showClose && (
|
|
||||||
<IconButton onClick={onClose}>
|
|
||||||
{theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</AppNavigationHeader>
|
|
||||||
<Divider />
|
|
||||||
<List>
|
|
||||||
<AppNavigationSection items={nav} />
|
|
||||||
</List>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMobileQuery = theme.breakpoints.down("md");
|
export const isMobileQuery = theme.breakpoints.down("md");
|
||||||
|
|
||||||
export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
export default function NavigationMenu({ anchorEl, open, onClose }: {
|
||||||
const isMobile = useMediaQuery(isMobileQuery);
|
anchorEl: PopoverProps["anchorEl"];
|
||||||
|
open: boolean;
|
||||||
return isMobile ? (
|
onClose: () => void
|
||||||
<>
|
}) {
|
||||||
<Box sx={{ flexShrink: 0, width: DRAWER_WIDTH }} />
|
return (
|
||||||
<Drawer
|
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
|
||||||
keepMounted
|
<AppNavigation />
|
||||||
anchor="right"
|
</Menu>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user