mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
New page layout (#33)
* added sidebar * add side-bar layout * fixed small bugs, adding mobile sidebar * fixed mobile sidebar * added mobile breadcrumb * improved project switcher order * fixed eslint * added typography * added page layout, removed unused data * new page layout * removed unused files
This commit is contained in:
parent
6f19e662df
commit
fddce41aa8
@ -11,6 +11,7 @@ import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"
|
||||
import EnvKeys from "@/components/env-keys";
|
||||
import { SmartLink } from "@/components/smart-link";
|
||||
import { ApiKeySetFirstView } from "@stackframe/stack";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
|
||||
export default function ApiKeysDashboardClient() {
|
||||
@ -20,22 +21,15 @@ export default function ApiKeysDashboardClient() {
|
||||
const [isNewApiKeyDialogOpen, setIsNewApiKeyDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Box>
|
||||
API Keys
|
||||
</Box>
|
||||
<AsyncButton onClick={() => setIsNewApiKeyDialogOpen(true)}>
|
||||
Create new
|
||||
</AsyncButton>
|
||||
</Stack>
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph body>
|
||||
Please note that API keys cannot be viewed anymore after they have been created. If you lose them, you will have to create new ones.
|
||||
</Paragraph>
|
||||
|
||||
<PageLayout
|
||||
title="API Keys"
|
||||
description="Manage your project's API keys"
|
||||
actions={
|
||||
<AsyncButton onClick={() => setIsNewApiKeyDialogOpen(true)}>
|
||||
Create new
|
||||
</AsyncButton>
|
||||
}
|
||||
>
|
||||
<ApiKeysTable rows={apiKeySets} />
|
||||
|
||||
<CreateNewDialog
|
||||
@ -43,7 +37,7 @@ export default function ApiKeysDashboardClient() {
|
||||
open={isNewApiKeyDialogOpen}
|
||||
onClose={() => setIsNewApiKeyDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import { redirectHandler } from "@/route-handlers/redirect-handler";
|
||||
|
||||
export const GET = redirectHandler("users");
|
||||
@ -10,6 +10,7 @@ import { SmartLink } from "@/components/smart-link";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
import { SimpleCard } from "@/components/simple-card";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
export default function EnvironmentClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
@ -21,12 +22,7 @@ export default function EnvironmentClient() {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Environment
|
||||
</Paragraph>
|
||||
|
||||
|
||||
<PageLayout title="Environment" description="Development and production settings">
|
||||
<SimpleCard title="Production mode">
|
||||
<SmartSwitch
|
||||
checked={project.isProductionMode}
|
||||
@ -78,6 +74,6 @@ export default function EnvironmentClient() {
|
||||
</IconAlert>
|
||||
)}
|
||||
</SimpleCard>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Sheet, SheetProps, Select, Option, SelectOption, Stack, Typography } from "@mui/joy";
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/joy/Box';
|
||||
import IconButton from '@mui/joy/IconButton';
|
||||
import { useAdminApp } from './use-admin-app';
|
||||
import { redirect, usePathname } from 'next/navigation';
|
||||
import { useUser } from '@stackframe/stack';
|
||||
import { Icon } from '@/components/icon';
|
||||
import Breadcrumbs from '@mui/joy/Breadcrumbs';
|
||||
import { NavigationItem } from "./navigation-data";
|
||||
|
||||
|
||||
function ProjectSwitchItem({ label }: { label: string }) {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 'sm',
|
||||
bgcolor: 'background.level1',
|
||||
marginRight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Typography>
|
||||
{label?.slice(0,1)?.toUpperCase()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level="title-md">{label}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectSwitch() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const user = useUser({ or: 'redirect', projectIdMustMatch: "internal" });
|
||||
const projects = user.useOwnedProjects();
|
||||
const project = projects.find((project) => project.id === stackAdminApp.projectId);
|
||||
|
||||
const renderValue = (option: SelectOption<string> | null) => {
|
||||
if (!option || typeof option.label !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ProjectSwitchItem label={option.label} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
// indicator={null}
|
||||
variant="plain"
|
||||
defaultValue={project?.id}
|
||||
size="sm"
|
||||
slotProps={{
|
||||
listbox: {
|
||||
sx: {
|
||||
zIndex: 10001
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={renderValue}
|
||||
onChange={(event, newProjectId) => {
|
||||
redirect(`/projects/${newProjectId}/users`);
|
||||
}}
|
||||
>
|
||||
{projects.map((projectInfo) => (
|
||||
<Option value={projectInfo.id} label={projectInfo.displayName} key={projectInfo.id}>
|
||||
<ProjectSwitchItem label={projectInfo.displayName} />
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header(props: SheetProps & {
|
||||
headerHeight: number,
|
||||
isCompactMediaQuery: string,
|
||||
onShowSidebar: () => void,
|
||||
navigationItems: NavigationItem[],
|
||||
}) {
|
||||
const { isCompactMediaQuery, onShowSidebar, navigationItems, headerHeight, ...sheetProps } = props;
|
||||
const pathname = usePathname();
|
||||
|
||||
const selectedItem = React.useMemo(() => {
|
||||
return navigationItems.find((item) => {
|
||||
if (item.type === 'label') {
|
||||
return false;
|
||||
}
|
||||
return item.regex.test(pathname);
|
||||
});
|
||||
}, [pathname, navigationItems]);
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
variant="outlined"
|
||||
component="header"
|
||||
{...sheetProps}
|
||||
sx={{
|
||||
paddingX: 1,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 30,
|
||||
borderTop: 'none',
|
||||
borderLeft: 'none',
|
||||
borderRight: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
height: `${headerHeight}px`,
|
||||
flexShrink: 0,
|
||||
...sheetProps.sx ?? {},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
flexGrow={1}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="outlined"
|
||||
onClick={onShowSidebar}
|
||||
sx={{
|
||||
display: 'none',
|
||||
[isCompactMediaQuery]: {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon icon="menu" />
|
||||
</IconButton>
|
||||
<Stack flexDirection="row" alignItems="center">
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
<ProjectSwitch />
|
||||
{selectedItem ? (
|
||||
typeof selectedItem.name === 'string' ? (
|
||||
<Typography level="title-md">{selectedItem.name}</Typography>
|
||||
) : selectedItem.name.map((name, index) => (
|
||||
<Typography key={name} level="title-md">{name}</Typography>
|
||||
))
|
||||
) : null}
|
||||
|
||||
</Breadcrumbs>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
import { Icon } from "@/components/icon";
|
||||
|
||||
type Label = {
|
||||
name: string,
|
||||
type: 'label',
|
||||
};
|
||||
|
||||
type Item = {
|
||||
name: string,
|
||||
href: string,
|
||||
icon: JSX.Element,
|
||||
regex: RegExp,
|
||||
type: 'item',
|
||||
};
|
||||
|
||||
type Hidden = {
|
||||
name: string | string[],
|
||||
regex: RegExp,
|
||||
type: 'hidden',
|
||||
};
|
||||
|
||||
export type NavigationItem = Label | Item | Hidden;
|
||||
|
||||
export const navigationItems: (Label | Item | Hidden)[] = [
|
||||
{
|
||||
name: "Users",
|
||||
type: 'label'
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
href: "/users",
|
||||
regex: /^\/projects\/[^\/]+\/user\/[^\/]+$/,
|
||||
icon: <Icon icon="person" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Auth Methods",
|
||||
href: "/providers",
|
||||
regex: /^\/projects\/[^\/]+\/providers$/,
|
||||
icon: <Icon icon="security" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Teams",
|
||||
type: 'label'
|
||||
},
|
||||
{
|
||||
name: "Teams",
|
||||
href: "/teams",
|
||||
regex: /^\/projects\/[^\/]+\/teams$/,
|
||||
icon: <Icon icon="group" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: ["Team", "Members"],
|
||||
regex: /^\/projects\/[^\/]+\/teams\/[^\/]+$/,
|
||||
type: "hidden",
|
||||
},
|
||||
{
|
||||
name: "Permissions",
|
||||
href: "/team-permissions",
|
||||
regex: /^\/projects\/[^\/]+\/team-permissions$/,
|
||||
icon: <Icon icon="lock" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
type: 'label'
|
||||
},
|
||||
{
|
||||
name: "Domains & Handlers",
|
||||
href: "/urls-and-callbacks",
|
||||
regex: /^\/projects\/[^\/]+\/urls-and-callbacks$/,
|
||||
icon: <Icon icon="link" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Team Settings",
|
||||
href: "/team-settings",
|
||||
regex: /^\/projects\/[^\/]+\/team-settings$/,
|
||||
icon: <Icon icon="settings" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
href: "/environment",
|
||||
regex: /^\/projects\/[^\/]+\/environment$/,
|
||||
icon: <Icon icon="list_alt" />,
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/api-keys",
|
||||
regex: /^\/projects\/[^\/]+\/api-keys$/,
|
||||
icon: <Icon icon="key" />,
|
||||
type: 'item'
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,29 @@
|
||||
import Typography from "@/components/ui/typography";
|
||||
|
||||
export function PageLayout(props: {
|
||||
children: React.ReactNode,
|
||||
title: string,
|
||||
description?: string,
|
||||
actions?: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<div className="px-4 py-2">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<Typography type="h2">
|
||||
{props.title}
|
||||
</Typography>
|
||||
{props.description && (
|
||||
<Typography type="p" variant="secondary">
|
||||
{props.description}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
{props.actions}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { SimpleCard } from "@/components/simple-card";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { ProviderAccordion, availableProviders } from "./provider-accordion";
|
||||
import { OAuthProviderConfigJson } from "@stackframe/stack-shared";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
export default function ProvidersClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
@ -14,10 +15,7 @@ export default function ProvidersClient() {
|
||||
const oauthProviders = project.evaluatedConfig.oauthProviders;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Auth Methods
|
||||
</Paragraph>
|
||||
<PageLayout title="Auth Methods" description="Configure how users can sign in to your app">
|
||||
|
||||
<Paragraph body>
|
||||
<SimpleCard title="Email authentication">
|
||||
@ -88,6 +86,6 @@ export default function ProvidersClient() {
|
||||
In order to add a new provider, you can choose to use shared credentials created by us, or create your own OAuth client on the provider's website.
|
||||
</Paragraph>
|
||||
</SimpleCard>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
"use client";;
|
||||
import { Drawer, Stack, useTheme } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Header } from "./header";
|
||||
import { navigationItems } from "./navigation-data";
|
||||
|
||||
export default function SidebarLayout(props: { children: React.ReactNode, params: { projectId: string } }) {
|
||||
const theme = useTheme();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const isCompactMediaQuery = theme.breakpoints.down("md");
|
||||
|
||||
const headerHeight = 50;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
flexGrow={1}
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Sidebar
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
mode='full'
|
||||
/>
|
||||
<Stack flexGrow={1} direction="column" sx={{ overflow: 'hidden', height: '100vh' }}>
|
||||
<Header
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
onShowSidebar={() => setIsSidebarOpen(true)}
|
||||
/>
|
||||
<Stack
|
||||
paddingX={{ md: 4, xs: 2 }}
|
||||
flexGrow={1}
|
||||
paddingY={2}
|
||||
minWidth={0}
|
||||
overflow='auto'
|
||||
>
|
||||
<Stack spacing={2} component="main">
|
||||
{props.children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Drawer
|
||||
open={isSidebarOpen}
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
>
|
||||
<Sidebar
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
mode='compact'
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -34,6 +34,7 @@ import { ProjectSwitcher } from "@/components/project-switcher";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { DropdownMenu } from "@radix-ui/react-dropdown-menu";
|
||||
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import Typography from "@/components/ui/typography";
|
||||
|
||||
type Label = {
|
||||
name: string,
|
||||
@ -163,9 +164,9 @@ export function SidebarContent({ projectId, onNavigate }: { projectId: string, o
|
||||
<div className="flex flex-col gap-1 pt-2">
|
||||
{navigationItems.map((item, index) => {
|
||||
if (item.type === 'label') {
|
||||
return <div key={index} className="pl-2 text-sm text-gray-500">
|
||||
return <Typography key={index} className="pl-2" type="label" variant="secondary">
|
||||
{item.name}
|
||||
</div>;
|
||||
</Typography>;
|
||||
} else if (item.type === 'item') {
|
||||
return <div key={index} className="flex px-1">
|
||||
<NavItem item={item} projectId={projectId} onClick={onNavigate} />
|
||||
@ -190,8 +191,7 @@ export function SidebarContent({ projectId, onNavigate }: { projectId: string, o
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
{/* <div className="m-2 p-2 border rounded-md self-stretch"> */}
|
||||
<div className="px-2 flex items-center">
|
||||
<div className="px-4 py-2 flex items-center">
|
||||
<UserButton showUserInfo colorModeToggle={() => setMode(mode === 'light' ? 'dark' : 'light')} />
|
||||
</div>
|
||||
</div>
|
||||
@ -319,7 +319,7 @@ export default function SidebarLayout(props: { projectId: string, children?: Rea
|
||||
Feedback
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
'use client';;
|
||||
import * as React from 'react';
|
||||
import NextLink from 'next/link';
|
||||
import Box from '@mui/joy/Box';
|
||||
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 './use-admin-app';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { UserButton } from '@stackframe/stack';
|
||||
import { useColorScheme, Stack, Sheet } from '@mui/joy';
|
||||
import { Icon } from '@/components/icon';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { NavigationItem } from './navigation-data';
|
||||
|
||||
|
||||
function SidebarItem({
|
||||
title,
|
||||
icon,
|
||||
href,
|
||||
target,
|
||||
}: {
|
||||
title: string,
|
||||
icon: React.ReactNode,
|
||||
href: string,
|
||||
target?: 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}
|
||||
target={target}
|
||||
component={NextLink}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "background.level1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<ListItemContent>
|
||||
<Typography level="title-md">{title}</Typography>
|
||||
</ListItemContent>
|
||||
{target === "_blank" ? <Icon icon="open_in_new" /> : null}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar(props: {
|
||||
isCompactMediaQuery: string,
|
||||
headerHeight: number,
|
||||
navigationItems: NavigationItem[],
|
||||
mode: 'compact' | 'full',
|
||||
}) {
|
||||
const { mode, setMode } = useColorScheme();
|
||||
const stackAdminApp = useAdminApp();
|
||||
const basePath = `/projects/${stackAdminApp.projectId}`;
|
||||
|
||||
const { headerHeight, navigationItems, ...sheetProps} = props;
|
||||
return (
|
||||
<Sheet
|
||||
variant="outlined"
|
||||
sx={props.mode === 'full' ? {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
height: `100vh`,
|
||||
[props.isCompactMediaQuery]: {
|
||||
top: `${headerHeight}px`,
|
||||
height: `calc(100vh - ${headerHeight}px)`,
|
||||
},
|
||||
overflowY: 'auto',
|
||||
borderLeft: 'none',
|
||||
borderTop: 'none',
|
||||
borderBottom: 'none',
|
||||
width: '250px',
|
||||
flexShrink: 0,
|
||||
display: 'block',
|
||||
[props.isCompactMediaQuery]: { display: 'none' },
|
||||
} : {}}>
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100dvh',
|
||||
top: 0,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<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',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<List size="sm" sx={{ gap: 0.25 }}>
|
||||
{navigationItems.map((item) => (
|
||||
item.type === 'item' ?
|
||||
<SidebarItem
|
||||
key={item.name + '-item'}
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
href={basePath + item.href}
|
||||
/> :
|
||||
item.type === 'label' ?
|
||||
<Typography
|
||||
key={item.name + '-label'}
|
||||
level="title-sm"
|
||||
color="neutral"
|
||||
sx={{ my: 1 }}
|
||||
>
|
||||
{item.name}
|
||||
</Typography> : null
|
||||
))}
|
||||
<Box style={{ flexGrow: 1 }}/>
|
||||
<SidebarItem
|
||||
title='Documentation'
|
||||
icon={<Icon icon="help_outline" />}
|
||||
href={process.env.NEXT_PUBLIC_DOC_URL || ''}
|
||||
target="_blank"
|
||||
/>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mt: 1 }} />
|
||||
|
||||
<Box sx={{ py: 1 }}>
|
||||
<UserButton showUserInfo colorModeToggle={() => setMode(mode === 'light' ? 'dark' : 'light')} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
import { Button, Input, InputProps } from "@mui/joy";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Icon } from "@/components/icon";
|
||||
|
||||
export function SiteSearch(props: InputProps) {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const { ...inputProps } = props;
|
||||
|
||||
const openSearch = useCallback(() => {
|
||||
const baseUrl = new URL(process.env.__NEXT_ROUTER_BASEPATH || "", window.location.origin);
|
||||
// Let's strip away all information but the necessary (Google search doesn't support eg. port number)
|
||||
const baseSite = `${baseUrl.hostname}${baseUrl.pathname}`;
|
||||
|
||||
window.open(`https://www.google.com/search?q=${encodeURIComponent(`${searchText} site:${baseSite}`)}`, '_blank');
|
||||
}, [searchText]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
placeholder="Search"
|
||||
startDecorator={<Icon icon="search" sx={{
|
||||
color: theme => theme.palette.primary[500],
|
||||
}} />}
|
||||
endDecorator={
|
||||
<Button size="sm" color="primary" variant="solid" onClick={() => openSearch()}>
|
||||
Go
|
||||
</Button>
|
||||
}
|
||||
{...inputProps}
|
||||
sx={{
|
||||
'&:not(:focus-within) button:not(:active)': {
|
||||
display: 'none',
|
||||
},
|
||||
...inputProps.sx ?? {},
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
openSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -21,6 +21,7 @@ import {
|
||||
import { Icon } from "@/components/icon";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { PermissionGraph, PermissionList } from "./permission-list";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
|
||||
export default function ClientPage() {
|
||||
@ -29,16 +30,14 @@ export default function ClientPage() {
|
||||
const [createPermissionModalOpen, setCreatePermissionModalOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Team Permissions
|
||||
</Paragraph>
|
||||
|
||||
<Stack alignItems={"flex-start"}>
|
||||
<PageLayout
|
||||
title="Team Permissions"
|
||||
description="Manage team permissions"
|
||||
actions={
|
||||
<AsyncButton onClick={() => setCreatePermissionModalOpen(true)}>
|
||||
Create Permission
|
||||
</AsyncButton>
|
||||
</Stack>
|
||||
}>
|
||||
|
||||
<PermissionsTable rows={permissions} />
|
||||
|
||||
@ -46,7 +45,7 @@ export default function ClientPage() {
|
||||
open={createPermissionModalOpen}
|
||||
onClose={() => setCreatePermissionModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -4,18 +4,14 @@ import { Paragraph } from "@/components/paragraph";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
import { SimpleCard } from "@/components/simple-card";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
export default function TeamSettingsClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Environment
|
||||
</Paragraph>
|
||||
|
||||
|
||||
<PageLayout title="Team Settings" description="Configure how teams are created and managed">
|
||||
<SimpleCard title="Automatic Team Creation">
|
||||
<SmartSwitch
|
||||
checked={project.evaluatedConfig.createTeamOnSignUp}
|
||||
@ -33,6 +29,6 @@ export default function TeamSettingsClient() {
|
||||
</Typography>
|
||||
</SmartSwitch>
|
||||
</SimpleCard>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { MemberTable } from './member-table';
|
||||
import { useAdminApp } from '../../use-admin-app';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { SmartSwitch } from '@/components/smart-switch';
|
||||
import { PageLayout } from '../../page-layout';
|
||||
|
||||
|
||||
export default function ClientPage(props: { teamId: string }) {
|
||||
@ -18,14 +19,11 @@ export default function ClientPage(props: { teamId: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
{team.displayName}
|
||||
</Paragraph>
|
||||
<PageLayout title="Team Members" description={`Manage team members of "${team.displayName}"`}>
|
||||
|
||||
<Paragraph body>
|
||||
<MemberTable rows={users || []} team={team} />
|
||||
</Paragraph>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { TeamTable } from "./team-table";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
|
||||
export default function ClientPage() {
|
||||
@ -9,12 +10,8 @@ export default function ClientPage() {
|
||||
const teams = stackAdminApp.useTeams();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Teams
|
||||
</Paragraph>
|
||||
|
||||
<PageLayout title="Teams" description="Manage your project's teams">
|
||||
<TeamTable rows={teams} />
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { useAdminApp } from "../use-admin-app";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
import { Project } from "@stackframe/stack";
|
||||
import { DomainConfigJson } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { PageLayout } from "../page-layout";
|
||||
|
||||
function isValidUrl(urlString: string) {
|
||||
try {
|
||||
@ -155,10 +156,7 @@ export default function UrlsAndCallbacksClient() {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Domains & Handlers
|
||||
</Paragraph>
|
||||
<PageLayout title="Domains and Handler" description="Specify trusted domains and handler URLs">
|
||||
|
||||
<SimpleCard title="Domains and Handler">
|
||||
<Box sx={{ my: 2 }}>
|
||||
@ -273,6 +271,6 @@ export default function UrlsAndCallbacksClient() {
|
||||
Your project will no longer be able to receive callbacks from this domain.
|
||||
</Paragraph>
|
||||
</Dialog>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { UsersTable } from "./users-table";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { Alert } from "@mui/joy";
|
||||
import { SmartLink } from "@/components/smart-link";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
|
||||
|
||||
export default function UsersDashboardClient() {
|
||||
@ -12,15 +13,10 @@ export default function UsersDashboardClient() {
|
||||
const allUsers = stackAdminApp.useServerUsers();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph h1>
|
||||
Users
|
||||
</Paragraph>
|
||||
|
||||
|
||||
<PageLayout title="Users" description="Manage your project's users">
|
||||
{allUsers.length > 0 ? null : (
|
||||
<Paragraph body>
|
||||
<Alert color="success">
|
||||
<Alert className="mb-4">
|
||||
Congratulations on starting your project! Check the <SmartLink href="https://docs.stack-auth.com">documentation</SmartLink> to add your first users.
|
||||
</Alert>
|
||||
</Paragraph>
|
||||
@ -29,6 +25,6 @@ export default function UsersDashboardClient() {
|
||||
<Paragraph body>
|
||||
<UsersTable rows={allUsers} />
|
||||
</Paragraph>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { Text, UserButton } from "@stackframe/stack";
|
||||
import { UserButton } from "@stackframe/stack";
|
||||
import { Logo } from "./logo";
|
||||
import { Separator } from "./ui/separator";
|
||||
import Link from "next/link";
|
||||
import { useColorScheme } from "@mui/joy";
|
||||
import Typography from "./ui/typography";
|
||||
|
||||
export function Navbar({ ...props }) {
|
||||
const { mode, setMode } = useColorScheme();
|
||||
@ -20,10 +21,10 @@ export function Navbar({ ...props }) {
|
||||
<div className="flex items-center">
|
||||
<div className="flex gap-4 mr-8 items-center">
|
||||
<Link href="mailto:team@stack-auth.com">
|
||||
<Text size='sm' variant='secondary'>Feedback</Text>
|
||||
<Typography type='label'>Feedback</Typography>
|
||||
</Link>
|
||||
<Link href="https://docs.stack-auth.com/">
|
||||
<Text size='sm' variant='secondary'>Docs</Text>
|
||||
<Typography type='label'>Docs</Typography>
|
||||
</Link>
|
||||
</div>
|
||||
<UserButton colorModeToggle={() => setMode(mode === 'light' ? 'dark' : 'light')}/>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client';;
|
||||
import { CardDescription, CardFooter, CardHeader, CardTitle, ClickableCard } from './ui/card';
|
||||
import { CardContent } from '@mui/joy';
|
||||
import { Project } from '@stackframe/stack';
|
||||
import { useFromNow } from '@/hooks/use-from-now';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Typography from './ui/typography';
|
||||
|
||||
export function ProjectCard({ project }: { project: Project }) {
|
||||
const createdAt = useFromNow(project.createdAt);
|
||||
@ -15,15 +15,13 @@ export function ProjectCard({ project }: { project: Project }) {
|
||||
<CardTitle>{project.displayName}</CardTitle>
|
||||
<CardDescription>{project.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<p className='text-sm text-gray-500'>
|
||||
<Typography type='label' variant='secondary'>
|
||||
{project.userCount} users
|
||||
</p>
|
||||
<p className='text-sm text-gray-500'>
|
||||
</Typography>
|
||||
<Typography type='label' variant='secondary'>
|
||||
{createdAt}
|
||||
</p>
|
||||
</Typography>
|
||||
</CardFooter>
|
||||
</ClickableCard>
|
||||
);
|
||||
|
||||
47
packages/stack-server/src/components/ui/typography.tsx
Normal file
47
packages/stack-server/src/components/ui/typography.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import React from "react";
|
||||
|
||||
export const typographyVariants = cva("text-md", {
|
||||
variants: {
|
||||
type: {
|
||||
h1: "text-4xl font-bold",
|
||||
h2: "text-3xl font-bold",
|
||||
h3: "text-2xl font-semibold",
|
||||
h4: "text-xl",
|
||||
p: "text-md",
|
||||
label: "text-sm",
|
||||
footnote: "text-xs",
|
||||
},
|
||||
variant: {
|
||||
primary: "text-black dark:text-white",
|
||||
secondary: "text-zinc-500",
|
||||
destructive: "text-red-500",
|
||||
success: "text-green-500",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "p",
|
||||
variant: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
export interface TypographyProps
|
||||
extends React.HTMLAttributes<HTMLHeadingElement>,
|
||||
VariantProps<typeof typographyVariants> {}
|
||||
|
||||
const Typography = React.forwardRef<HTMLHeadingElement, TypographyProps>(
|
||||
({ className, type, variant, ...props }, ref) => {
|
||||
const Comp = (type === 'footnote' || type === 'label' ? 'p' : type) || 'p';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(typographyVariants({ type, variant, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Typography.displayName = "Typography";
|
||||
|
||||
export default Typography;
|
||||
Loading…
Reference in New Issue
Block a user