added header navigation

This commit is contained in:
Zai Shi 2024-02-28 16:31:41 +01:00
parent 6223aea526
commit 9280955995
5 changed files with 221 additions and 185 deletions

View File

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

View File

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

View File

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

View File

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

View File

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