mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
added header navigation
This commit is contained in:
parent
6223aea526
commit
9280955995
@ -5,18 +5,14 @@ import * as React from 'react';
|
||||
import Box from '@mui/joy/Box';
|
||||
import IconButton from '@mui/joy/IconButton';
|
||||
import { useAdminApp } from './useAdminInterface';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { redirect, usePathname } from 'next/navigation';
|
||||
import { useStrictMemo } from 'stack-shared/src/hooks/use-strict-memo';
|
||||
import { useStackApp } from 'stack';
|
||||
import { Icon } from '@/components/icon';
|
||||
import { Logo } from '@/components/logo';
|
||||
import Breadcrumbs from '@mui/joy/Breadcrumbs';
|
||||
|
||||
|
||||
function ProjectSwitchItem({
|
||||
label
|
||||
}: {
|
||||
label: string,
|
||||
}) {
|
||||
function ProjectSwitchItem({ label }: { label: string }) {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
@ -56,7 +52,7 @@ function ProjectSwitch() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 0.5 }}>
|
||||
<Box>
|
||||
<ProjectSwitchItem label={option.label} />
|
||||
</Box>
|
||||
);
|
||||
@ -64,6 +60,7 @@ function ProjectSwitch() {
|
||||
|
||||
return (
|
||||
<Select
|
||||
// indicator={null}
|
||||
variant="plain"
|
||||
defaultValue={project?.id}
|
||||
size="sm"
|
||||
@ -91,8 +88,19 @@ function ProjectSwitch() {
|
||||
export function Header(props: SheetProps & {
|
||||
isCompactMediaQuery: string,
|
||||
onShowSidebar: () => void,
|
||||
navigationItems: { name: string, href: string, icon: React.ReactNode }[],
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const { isCompactMediaQuery, onShowSidebar, ...sheetProps } = props;
|
||||
const basePath = `/projects/${stackAdminApp.projectId}`;
|
||||
const pathname = usePathname();
|
||||
|
||||
const selectedItem = React.useMemo(() => {
|
||||
return props.navigationItems.find((item) => {
|
||||
return new URL(basePath + item.href, "https://example.com").pathname === pathname;
|
||||
});
|
||||
}, [pathname, basePath, props.navigationItems]);
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
variant="outlined"
|
||||
@ -131,7 +139,12 @@ export function Header(props: SheetProps & {
|
||||
>
|
||||
<Icon icon="menu" />
|
||||
</IconButton>
|
||||
<ProjectSwitch />
|
||||
<Stack flexDirection="row" alignItems="center">
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
<ProjectSwitch />
|
||||
<Typography level="title-md">{selectedItem?.name}</Typography>
|
||||
</Breadcrumbs>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@ -6,6 +6,41 @@ import { Logo } from "@/components/logo";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { AdminAppProvider } from "./useAdminInterface";
|
||||
import { Header } from "./header";
|
||||
import { Icon } from '@/components/icon';
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
href: "",
|
||||
icon: <Icon icon="grid_view" />,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
href: "/auth/users",
|
||||
icon: <Icon icon="people_outline" />,
|
||||
},
|
||||
{
|
||||
name: "Providers",
|
||||
href: "/auth/providers",
|
||||
icon: <Icon icon="device_hub" />,
|
||||
},
|
||||
{
|
||||
name: "Domains & Handlers",
|
||||
href: "/auth/urls-and-callbacks",
|
||||
icon: <Icon icon="link" />,
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
href: "/settings/environment",
|
||||
icon: <Icon icon="list_alt" />,
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/settings/api-keys",
|
||||
icon: <Icon icon="key" />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode, params: { projectId: string } }) {
|
||||
const theme = useTheme();
|
||||
@ -24,6 +59,7 @@ export default function Layout(props: { children: React.ReactNode, params: { pro
|
||||
>
|
||||
<Sidebar
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
@ -45,6 +81,7 @@ export default function Layout(props: { children: React.ReactNode, params: { pro
|
||||
/>
|
||||
<Stack flexGrow={1} direction="column">
|
||||
<Header
|
||||
navigationItems={navigationItems}
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
onShowSidebar={() => setIsSidebarOpen(true)}
|
||||
sx={{
|
||||
@ -86,8 +123,9 @@ export default function Layout(props: { children: React.ReactNode, params: { pro
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
>
|
||||
<Sidebar
|
||||
onCloseSidebar={() => setIsSidebarOpen(false)}
|
||||
// onCloseSidebar={() => setIsSidebarOpen(false)}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
/>
|
||||
</Drawer>
|
||||
</AdminAppProvider>
|
||||
|
||||
@ -1,160 +0,0 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import NextLink from 'next/link';
|
||||
import Avatar from '@mui/joy/Avatar';
|
||||
import Box from '@mui/joy/Box';
|
||||
import Button from '@mui/joy/Button';
|
||||
import Divider from '@mui/joy/Divider';
|
||||
import List from '@mui/joy/List';
|
||||
import ListItem from '@mui/joy/ListItem';
|
||||
import ListItemButton from '@mui/joy/ListItemButton';
|
||||
import ListItemContent from '@mui/joy/ListItemContent';
|
||||
import Typography from '@mui/joy/Typography';
|
||||
import { useAdminApp } from './useAdminInterface';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUser } from 'stack';
|
||||
import { Dropdown, MenuButton, MenuItem, Menu, useColorScheme, Stack, Link } from '@mui/joy';
|
||||
import { Icon } from '@/components/icon';
|
||||
import { AsyncButton } from '@/components/async-button';
|
||||
import { Logo } from '@/components/logo';
|
||||
|
||||
|
||||
function SidebarItem({
|
||||
title,
|
||||
icon,
|
||||
href,
|
||||
}: {
|
||||
title: string,
|
||||
icon: React.ReactNode,
|
||||
href: string,
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const selected = React.useMemo(() => {
|
||||
return pathname === new URL(href, "https://example.com").pathname;
|
||||
}, [href, pathname]);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
'--ListItem-radius': (theme) => theme.vars.radius.sm,
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
selected={selected}
|
||||
href={href}
|
||||
component={NextLink}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "background.level1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<ListItemContent>
|
||||
<Typography level="title-md">{title}</Typography>
|
||||
</ListItemContent>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarSection() {
|
||||
const { mode, setMode } = useColorScheme();
|
||||
const user = useUser({ or: 'redirect' });
|
||||
const nameStyle = {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Avatar
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
src={user.profileImageUrl || undefined}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography level="title-sm" sx={nameStyle}>{user.displayName || user.primaryEmail}</Typography>
|
||||
{user.displayName ? <Typography level="body-xs" sx={nameStyle}>{user.primaryEmail}</Typography> : null}
|
||||
</Box>
|
||||
<Dropdown>
|
||||
<MenuButton size="sm" variant="plain" color="neutral">
|
||||
<Icon icon="more_vert" />
|
||||
<Menu sx={{ zIndex: 10001 }}>
|
||||
<MenuItem>
|
||||
<Button onClick={() => setMode(mode === 'dark' ? 'light' : 'dark')} variant='plain' sx={{ width: '100%'}}>
|
||||
{mode === 'dark' ? <Icon icon="light_mode" sx={{ mr: 1 }}/> : <Icon icon="dark_mode" sx={{ mr: 1 }}/>}
|
||||
{mode === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<AsyncButton onClick={() => user.signOut()} variant='plain' sx={{ w: '100%'}}>
|
||||
<Icon icon="logout" sx={{ mr: 1 }} />
|
||||
Sign out
|
||||
</AsyncButton>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuButton>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarContents({ headerHeight } : { headerHeight: number }) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const basePath = `/projects/${stackAdminApp.projectId}`;
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100dvh',
|
||||
top: 0,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* <SidebarHeader /> */}
|
||||
|
||||
<Stack sx={{ marginLeft: 2, justifyContent: 'center', height: headerHeight - 1 }}>
|
||||
<Logo full height={24} href="/projects" />
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ mb: 1 }} />
|
||||
|
||||
<Stack sx={{ px: 1, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 0,
|
||||
overflow: 'hidden auto',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<List
|
||||
size="sm"
|
||||
sx={{ gap: 0.25 }}
|
||||
>
|
||||
<SidebarItem title="Dashboard" icon={<Icon icon="grid_view" />} href={basePath} />
|
||||
<SidebarItem title="Users" icon={<Icon icon="people_outline" />} href={`${basePath}/auth/users`} />
|
||||
<SidebarItem title="Providers" icon={<Icon icon="device_hub" />} href={`${basePath}/auth/providers`} />
|
||||
<SidebarItem title="Domains & Handlers" icon={<Icon icon="link" />} href={`${basePath}/auth/urls-and-callbacks`} />
|
||||
<SidebarItem title="Environment" icon={<Icon icon="list_alt" />} href={`${basePath}/settings/environment`} />
|
||||
<SidebarItem title="API Keys" icon={<Icon icon="key" />} href={`${basePath}/settings/api-keys`} />
|
||||
</List>
|
||||
|
||||
<Button size="sm" variant="solid">
|
||||
Upgrade plan
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mt: 1 }} />
|
||||
|
||||
<Box sx={{ py: 1 }}>
|
||||
<AvatarSection />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -1,22 +1,166 @@
|
||||
"use client";
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import NextLink from 'next/link';
|
||||
import Avatar from '@mui/joy/Avatar';
|
||||
import Box from '@mui/joy/Box';
|
||||
import Button from '@mui/joy/Button';
|
||||
import Divider from '@mui/joy/Divider';
|
||||
import List from '@mui/joy/List';
|
||||
import ListItem from '@mui/joy/ListItem';
|
||||
import ListItemButton from '@mui/joy/ListItemButton';
|
||||
import ListItemContent from '@mui/joy/ListItemContent';
|
||||
import Typography from '@mui/joy/Typography';
|
||||
import { useAdminApp } from './useAdminInterface';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUser } from 'stack';
|
||||
import { Dropdown, MenuButton, MenuItem, Menu, useColorScheme, Stack, Link, Sheet, SheetProps } from '@mui/joy';
|
||||
import { Icon } from '@/components/icon';
|
||||
import { AsyncButton } from '@/components/async-button';
|
||||
import { Logo } from '@/components/logo';
|
||||
|
||||
import { SheetProps, Sheet } from "@mui/joy";
|
||||
import React from "react";
|
||||
import { useAdminApp } from "./useAdminInterface";
|
||||
import { SidebarContents } from "./sidebar-contents";
|
||||
|
||||
export function Sidebar(props: SheetProps & {
|
||||
onCloseSidebar?: () => void,
|
||||
headerHeight: number,
|
||||
function SidebarItem({
|
||||
title,
|
||||
icon,
|
||||
href,
|
||||
}: {
|
||||
title: string,
|
||||
icon: React.ReactNode,
|
||||
href: string,
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const { onCloseSidebar = () => {}, ...sheetProps } = props;
|
||||
const pathname = usePathname();
|
||||
const selected = React.useMemo(() => {
|
||||
return pathname === new URL(href, "https://example.com").pathname;
|
||||
}, [href, pathname]);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
'--ListItem-radius': (theme) => theme.vars.radius.sm,
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
selected={selected}
|
||||
href={href}
|
||||
component={NextLink}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "background.level1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<ListItemContent>
|
||||
<Typography level="title-md">{title}</Typography>
|
||||
</ListItemContent>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarSection() {
|
||||
const { mode, setMode } = useColorScheme();
|
||||
const user = useUser({ or: 'redirect' });
|
||||
const nameStyle = {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
{...sheetProps}
|
||||
>
|
||||
<SidebarContents headerHeight={props.headerHeight} />
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Avatar
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
src={user.profileImageUrl || undefined}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography level="title-sm" sx={nameStyle}>{user.displayName || user.primaryEmail}</Typography>
|
||||
{user.displayName ? <Typography level="body-xs" sx={nameStyle}>{user.primaryEmail}</Typography> : null}
|
||||
</Box>
|
||||
<Dropdown>
|
||||
<MenuButton size="sm" variant="plain" color="neutral">
|
||||
<Icon icon="more_vert" />
|
||||
<Menu sx={{ zIndex: 10001 }}>
|
||||
<MenuItem>
|
||||
<Button onClick={() => setMode(mode === 'dark' ? 'light' : 'dark')} variant='plain' sx={{ width: '100%'}}>
|
||||
{mode === 'dark' ? <Icon icon="light_mode" sx={{ mr: 1 }}/> : <Icon icon="dark_mode" sx={{ mr: 1 }}/>}
|
||||
{mode === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<AsyncButton onClick={() => user.signOut()} variant='plain' sx={{ w: '100%'}}>
|
||||
<Icon icon="logout" sx={{ mr: 1 }} />
|
||||
Sign out
|
||||
</AsyncButton>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuButton>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar(props : SheetProps & {
|
||||
headerHeight: number,
|
||||
navigationItems: { name: string, href: string, icon: React.ReactNode }[],
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const basePath = `/projects/${stackAdminApp.projectId}`;
|
||||
|
||||
const { headerHeight, navigationItems, ...sheetProps} = props;
|
||||
return (
|
||||
<Sheet {...sheetProps}>
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100dvh',
|
||||
top: 0,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* <SidebarHeader /> */}
|
||||
|
||||
<Stack sx={{ marginLeft: 2, justifyContent: 'center', height: headerHeight - 1 }}>
|
||||
<Logo full height={24} href="/projects" />
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ mb: 1 }} />
|
||||
|
||||
<Stack sx={{ px: 1, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 0,
|
||||
overflow: 'hidden auto',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<List size="sm" sx={{ gap: 0.25 }}>
|
||||
{navigationItems.map((item) => (
|
||||
<SidebarItem
|
||||
key={item.name}
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
href={basePath + item.href}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Button size="sm" variant="solid">
|
||||
Upgrade plan
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mt: 1 }} />
|
||||
|
||||
<Box sx={{ py: 1 }}>
|
||||
<AvatarSection />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { Button, Card, CardContent, CardOverflow, Divider, FormControl, FormLabel, Grid, Input, Stack, Textarea, Typography } from "@mui/joy";
|
||||
import { use, useId, useRef, useState } from "react";
|
||||
import { useUser, useStackApp } from "stack";
|
||||
import { useStackApp } from "stack";
|
||||
import { prettyPrintWithMagnitudes } from "stack-shared/dist/utils/numbers";
|
||||
import { Dialog } from "@/components/dialog";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
@ -12,6 +12,7 @@ import { useStrictMemo } from "stack-shared/src/hooks/use-strict-memo";
|
||||
import { runAsynchronously } from "stack-shared/src/utils/promises";
|
||||
import { Project } from "stack/dist/lib/stack-app";
|
||||
|
||||
|
||||
export default function ProjectsPageClient() {
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
const stackApp = useStackApp({ projectIdMustMatch: "internal" });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user