This commit is contained in:
bipproduction
2025-10-07 16:51:02 +08:00
parent f61b2d2696
commit b9c783cee1
2 changed files with 137 additions and 118 deletions

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "bun --hot src/index.tsx", "dev": "bun --hot src/index.tsx",
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
"start": "NODE_ENV=production bun src/index.tsx" "start": "NODE_ENV=production bun src/index.tsx",
"seed": "bun prisma/seed.ts"
}, },
"dependencies": { "dependencies": {
"@elysiajs/cors": "^1.4.0", "@elysiajs/cors": "^1.4.0",

View File

@@ -4,7 +4,9 @@ import {
ActionIcon, ActionIcon,
AppShell, AppShell,
Avatar, Avatar,
Button,
Card, Card,
Divider,
Flex, Flex,
Group, Group,
NavLink, NavLink,
@@ -22,142 +24,158 @@ import {
IconDashboard IconDashboard
} from '@tabler/icons-react' } from '@tabler/icons-react'
import type { User } from 'generated/prisma' import type { User } from 'generated/prisma'
import { data, Outlet, useLocation, useNavigate } from 'react-router-dom' import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { default as clientRoute, default as clientRoutes } from '@/clientRoutes' import { default as clientRoute, default as clientRoutes } from '@/clientRoutes'
import apiFetch from '@/lib/apiFetch' import apiFetch from '@/lib/apiFetch'
import { showNotification } from '@mantine/notifications'
function Logout() {
return <Group>
<Button variant='transparent' size='compact-xs' onClick={async () => {
await apiFetch.auth.logout.delete()
localStorage.removeItem('token')
window.location.href = '/login'
}}>Logout</Button>
</Group>
}
export default function DashboardLayout() { export default function DashboardLayout() {
const [opened, setOpened] = useLocalStorage({ const [opened, setOpened] = useLocalStorage({
key: 'nav_open', key: 'nav_open',
defaultValue: true, defaultValue: true,
}) })
return ( return (
<AppShell <AppShell
padding="md" padding="md"
navbar={{ navbar={{
width: 260, width: 260,
breakpoint: 'sm', breakpoint: 'sm',
collapsed: { mobile: !opened, desktop: !opened }, collapsed: { mobile: !opened, desktop: !opened },
}} }}
> >
<AppShell.Navbar> <AppShell.Navbar>
<AppShell.Section> <AppShell.Section>
<Group justify="flex-end" p="xs"> <Group justify="flex-end" p="xs">
<Tooltip <Tooltip
label={opened ? 'Collapse navigation' : 'Expand navigation'} label={opened ? 'Collapse navigation' : 'Expand navigation'}
withArrow withArrow
> >
<ActionIcon <ActionIcon
variant="light" variant="light"
color="gray" color="gray"
onClick={() => setOpened(v => !v)} onClick={() => setOpened(v => !v)}
aria-label="Toggle navigation" aria-label="Toggle navigation"
radius="xl" radius="xl"
> >
{opened ? <IconChevronLeft /> : <IconChevronRight />} {opened ? <IconChevronLeft /> : <IconChevronRight />}
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Group> </Group>
</AppShell.Section> </AppShell.Section>
<AppShell.Section grow component={ScrollArea} flex={1}> <AppShell.Section grow component={ScrollArea} flex={1}>
<NavigationDashboard /> <NavigationDashboard />
</AppShell.Section> </AppShell.Section>
<AppShell.Section> <AppShell.Section>
<HostView /> <HostView />
</AppShell.Section> </AppShell.Section>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main> <AppShell.Main>
<Stack> <Stack>
<Paper withBorder shadow="md" radius="lg" p="md"> <Paper withBorder shadow="md" radius="lg" p="md">
<Flex align="center" gap="md"> <Flex align="center" gap="md">
{!opened && ( {!opened && (
<Tooltip label="Open navigation menu" withArrow> <Tooltip label="Open navigation menu" withArrow>
<ActionIcon <ActionIcon
variant="light" variant="light"
color="gray" color="gray"
onClick={() => setOpened(true)} onClick={() => setOpened(true)}
aria-label="Open navigation" aria-label="Open navigation"
radius="xl" radius="xl"
> >
<IconChevronRight /> <IconChevronRight />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}
<Title order={3} fw={600}> <Title order={3} fw={600}>
App Dashboard App Dashboard
</Title> </Title>
</Flex> </Flex>
</Paper> </Paper>
<Outlet /> <Outlet />
</Stack> </Stack>
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>
) )
} }
/* ----------------------- Host Info ----------------------- */ /* ----------------------- Host Info ----------------------- */
function HostView() { function HostView() {
const [host, setHost] = useState<User | null>(null) const [host, setHost] = useState<User | null>(null)
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
const {data} = await apiFetch.api.user.find.get() const { data } = await apiFetch.api.user.find.get()
setHost(data?.user ?? null) setHost(data?.user ?? null)
} }
fetchHost() fetchHost()
}, []) }, [])
return ( return (
<Card radius="lg" withBorder shadow="sm" p="md"> <Card radius="lg" withBorder shadow="sm" p="md">
{host ? ( {host ? (
<Flex gap="md" align="center"> <Stack>
<Avatar size="md" radius="xl" color="blue"> <Flex gap="md" align="center">
{host.name?.[0]} <Avatar size="md" radius="xl" color="blue">
</Avatar> {host.name?.[0]}
<Stack gap={2}> </Avatar>
<Text fw={600}>{host.name}</Text> <Stack gap={2}>
<Text size="sm" c="dimmed">{host.email}</Text> <Text fw={600}>{host.name}</Text>
</Stack> <Text size="sm" c="dimmed">{host.email}</Text>
</Flex> </Stack>
) : ( </Flex>
<Text size="sm" c="dimmed" ta="center"> <Divider />
No host information available <Logout />
</Text> </Stack>
)} ) : (
</Card> <Text size="sm" c="dimmed" ta="center">
) No host information available
</Text>
)}
</Card>
)
} }
/* ----------------------- Navigation ----------------------- */ /* ----------------------- Navigation ----------------------- */
function NavigationDashboard() { function NavigationDashboard() {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const isActive = (path: keyof typeof clientRoute) => const isActive = (path: keyof typeof clientRoute) =>
location.pathname.startsWith(clientRoute[path]) location.pathname.startsWith(clientRoute[path])
return ( return (
<Stack gap="xs" p="sm"> <Stack gap="xs" p="sm">
<NavLink <NavLink
active={isActive('/dashboard/landing')} active={isActive('/dashboard/landing')}
leftSection={<IconDashboard size={20} />} leftSection={<IconDashboard size={20} />}
label="Dashboard Overview" label="Dashboard Overview"
description="Quick summary and activity highlights" description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes['/dashboard/landing'])} onClick={() => navigate(clientRoutes['/dashboard/landing'])}
/> />
<NavLink <NavLink
active={isActive('/dashboard/apikey')} active={isActive('/dashboard/apikey')}
leftSection={<IconDashboard size={20} />} leftSection={<IconDashboard size={20} />}
label="Dashboard Overview" label="Dashboard Overview"
description="Quick summary and activity highlights" description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes['/dashboard/apikey'])} onClick={() => navigate(clientRoutes['/dashboard/apikey'])}
/> />
</Stack> </Stack>
) )
} }