chore: starter template
This commit is contained in:
90
apps/app/src/components/AppShell.tsx
Normal file
90
apps/app/src/components/AppShell.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { AppShell as MantineAppShell, Box, Button, NavLink, Stack } from '@pikku/mantine/core'
|
||||
import { Link, Outlet, useRouterState } from '@tanstack/react-router'
|
||||
import { useState, type FC } from 'react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { useLocale } from '@/i18n/config'
|
||||
import { signOut } from '@/lib/auth'
|
||||
import { Wordmark } from './Wordmark'
|
||||
import { LanguageSelector } from './LanguageSelector'
|
||||
import { ThemeSelector } from './ThemeSelector'
|
||||
|
||||
const HomeGlyph = () => <NavGlyph d="M3 12 12 4l9 8M5 10v9h5v-6h4v6h5v-9" />
|
||||
const SignOutGlyph = () => (
|
||||
<NavGlyph d="M16 17l5-5-5-5M21 12H9M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
)
|
||||
|
||||
function NavGlyph({ d }: { d: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d={d} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const AppShell: FC = () => {
|
||||
useLocale()
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||
const [isSigningOut, setIsSigningOut] = useState(false)
|
||||
|
||||
return (
|
||||
<MantineAppShell navbar={{ width: 236, breakpoint: 'sm' }} padding="xl">
|
||||
<MantineAppShell.Navbar p="md">
|
||||
<Stack h="100%" gap={4}>
|
||||
<Box px="xs" py="sm">
|
||||
<Wordmark name={m.app__name()} size={26} />
|
||||
</Box>
|
||||
|
||||
<Stack gap={2} mt="xs">
|
||||
<NavLink
|
||||
component={Link}
|
||||
to="/app"
|
||||
label={m.nav__home()}
|
||||
leftSection={<HomeGlyph />}
|
||||
active={pathname === '/app'}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="xs" mt="auto">
|
||||
<ThemeSelector />
|
||||
<LanguageSelector />
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
leftSection={<SignOutGlyph />}
|
||||
loading={isSigningOut}
|
||||
onClick={async () => {
|
||||
setIsSigningOut(true)
|
||||
try {
|
||||
await signOut()
|
||||
window.location.href = '/login'
|
||||
} finally {
|
||||
setIsSigningOut(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{m.app_shell__sign_out()}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</MantineAppShell.Navbar>
|
||||
|
||||
{/* Flex column so a full-height page (e.g. a chat) can fill the remaining
|
||||
viewport with just `flex: 1, minHeight: 0` — no viewport math needed.
|
||||
Content-sized pages are unaffected. */}
|
||||
<MantineAppShell.Main style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Outlet />
|
||||
</MantineAppShell.Main>
|
||||
</MantineAppShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user