NestJS backend rewrite; migrate client to FluentUI V9

This commit is contained in:
2025-09-18 15:48:08 +09:00
parent 32ff36a12c
commit 34529cea68
62 changed files with 5642 additions and 3679 deletions

View File

@ -1,15 +1,53 @@
import { AppShell, Avatar, Burger, Button, Flex, Group, Image, Menu, NavLink, rem, Text, useMantineColorScheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useMantineColorScheme } from '@mantine/core';
import { Outlet, useNavigate } from 'react-router-dom';
import { IconChevronDown, IconLogout, IconSettings, IconMoon, IconSun } from '@tabler/icons-react';
import { IconLogout, IconSettings, IconMoon, IconSun, IconMenu2, IconUser } from '@tabler/icons-react';
import { getUserData, logout, useAuthStore } from '../store/auth';
import { useEffect, useState } from 'react';
import { UserData } from '../interfaces/auth';
import { pages } from '../constants/app';
import { Button, Image, makeStyles, Menu, MenuButton, MenuItem, MenuList, MenuPopover, MenuTrigger, Text } from '@fluentui/react-components';
const useStyles = makeStyles({
root: {
display: 'grid',
gridTemplateRows: 'min-content auto',
height: '100vh',
maxHeight: '100vh',
overflow: 'hidden'
},
header: {
display: 'flex',
maxHeight: '3rem',
borderBottom: '1px solid var(--colorNeutralShadowKey)'
},
main: {
display: 'flex',
overflow: 'hidden',
width: '100%',
height: '100%',
},
navbar: {
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
maxWidth: '200px',
position: 'relative',
width: '100%',
height: '100%',
transition: 'max-width .2s ease-in-out',
borderRight: '1px solid var(--colorNeutralShadowKey)'
},
content: {
overflow: 'auto',
display: 'flex',
position: 'relative',
width: '100%',
height: '100%'
}
})
function DashboardLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(false)
const navigate = useNavigate()
const getPageTitle = () => {
@ -33,115 +71,201 @@ function DashboardLayout() {
const { colorScheme, setColorScheme } = useMantineColorScheme();
const classes = useStyles()
const [navbarOpen, setNavbarOpen] = useState(true)
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: desktopOpened ? 200 : 50,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened },
}}
>
<AppShell.Header>
<Flex h="100%" px="md" w='100%' align='center' gap='sm'>
<Group>
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
</Group>
<div className={classes.root}>
<div className={classes.header}>
<div style={{
display: 'flex',
height: '100%',
width: '100%',
alignItems: 'center',
gap: '0.75rem',
padding: '0.5rem 0.5rem 0.5rem 0.25rem',
}}>
<Button appearance='subtle' onClick={() => setNavbarOpen(!navbarOpen)} icon={<IconMenu2 />} />
<Group w='auto'>
<Text fw='600'>{getPageTitle()}</Text>
</Group>
<Text weight='bold' size={400}>
{getPageTitle()}
</Text>
<Group id='header-portal' w='auto' ml='auto'>
<div id='header-portal' style={{ marginLeft: 'auto' }}>
</Group>
</div>
<Group style={{ flexShrink: 0 }}>
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
withinPortal
>
<Menu.Target>
<Button variant='transparent'>
<Group gap={7}>
<Avatar name={`${userData?.name} ${userData?.surname}`} radius="xl" size={30} />
<Text fw={500} size="sm" lh={1} mr={3}>
{`${userData?.name} ${userData?.surname}`}
</Text>
<IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
</Group>
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{userData?.login}</Menu.Label>
<Menu.Item
leftSection={
colorScheme === 'dark' ? <IconMoon style={{ width: rem(16), height: rem(16) }} stroke={1.5} /> : <IconSun style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
onClick={() => colorScheme === 'dark' ? setColorScheme('light') : setColorScheme('dark')}
>
Тема: {colorScheme === 'dark' ? 'тёмная' : 'светлая'}
</Menu.Item>
<Menu.Item
leftSection={
<IconSettings style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
onClick={() => navigate('/settings')}
>
Настройки профиля
</Menu.Item>
<Menu.Item
onClick={() => {
<div style={{ flexShrink: 0 }}>
<Menu positioning={{ autoSize: true }}>
<MenuTrigger>
<MenuButton appearance='transparent' icon={<IconUser />}>{`${userData?.name} ${userData?.surname}`}</MenuButton>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem icon={
colorScheme === 'dark' ? <IconMoon /> : <IconSun />
} onClick={() => colorScheme === 'dark' ? setColorScheme('light') : setColorScheme('dark')}>Тема: {colorScheme === 'dark' ? 'тёмная' : 'светлая'}</MenuItem>
<MenuItem icon={<IconSettings />} onClick={() => navigate('/settings')}>Настройки профиля</MenuItem>
<MenuItem icon={<IconLogout />} onClick={() => {
logout()
navigate("/auth/signin")
}}
leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
>
Выход
</Menu.Item>
<Menu.Item>
<Flex gap='sm' align='center'>
<Image src={'/logo2.svg'} w={32} />
<Text>0.1.0</Text>
</Flex>
</Menu.Item>
</Menu.Dropdown>
}}>Выход</MenuItem>
<MenuItem icon={<Image src={'/logo2.svg'} width={24} />}>
0.1.0
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</Group>
</Flex>
</AppShell.Header>
<AppShell.Navbar style={{ transition: "width 0.2s ease" }}>
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item) => (
<NavLink
key={item.path}
onClick={() => navigate(item.path)}
label={item.label}
leftSection={item.icon}
active={location.pathname === item.path}
style={{ textWrap: 'nowrap' }}
// styles={(theme, { active }) => ({
// root: {
// color: active ? theme.colors.blue[6] : theme.colors.dark[5],
// fontWeight: active ? "bold" : "normal",
// },
// leftSection: {
// color: active ? theme.colors.blue[6] : theme.colors.dark[5], // Icon color
// }
// })}
/>
))}
</AppShell.Navbar>
<AppShell.Main>
<Flex bg={colorScheme === 'dark' ? undefined : '#E8E8E8'} w={{ sm: desktopOpened ? 'calc(100% - 200px)' : 'calc(100% - 50px)', base: '100%' }} h={'calc(100% - 60px)'} style={{ transition: "width 0.2s ease" }} pos={'fixed'}>
</div>
</div>
</div>
<div className={classes.main}>
<div className={classes.navbar} style={{
maxWidth: navbarOpen ? '200px' : '2.70rem',
}}>
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item) => (
<Button key={item.path} style={{ paddingLeft: '0.5rem', flexShrink: 0, flexWrap: 'nowrap', textWrap: 'nowrap', borderRadius: 0 }} appearance='subtle' onClick={() => navigate(item.path)}>
<div style={{ display: 'flex', }}>
{item.icon}
</div>
<div style={{
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
overflow: 'hidden',
marginLeft: '1rem',
}}>
{item.label}
</div>
</Button>
// <NavItem style={{ flexShrink: 0, flexWrap: 'nowrap', textWrap: 'nowrap' }} onClick={() => navigate(item.path)} icon={item.icon} value={item.path}>
// {item.label}
// </NavItem>
))}
</div>
<div className={classes.content}>
<Outlet />
</Flex>
</AppShell.Main>
</AppShell>
</div>
</div>
</div>
)
// return (
// <AppShell
// header={{ height: 60 }}
// navbar={{
// width: desktopOpened ? 200 : 50,
// breakpoint: 'sm',
// collapsed: { mobile: !mobileOpened },
// }}
// >
// <AppShell.Header>
// <Flex h="100%" px="md" w='100%' align='center' gap='sm'>
// <Group>
// <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
// <Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
// </Group>
// <Group w='auto'>
// <Text fw='600'>{getPageTitle()}</Text>
// </Group>
// <Group id='header-portal' w='auto' ml='auto'>
// </Group>
// <Group style={{ flexShrink: 0 }}>
// <Menu
// width={260}
// position="bottom-end"
// transitionProps={{ transition: 'pop-top-right' }}
// withinPortal
// >
// <Menu.Target>
// <Button variant='transparent'>
// <Group gap={7}>
// <Avatar name={`${userData?.name} ${userData?.surname}`} radius="xl" size={30} />
// <Text fw={500} size="sm" lh={1} mr={3}>
// {`${userData?.name} ${userData?.surname}`}
// </Text>
// <IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
// </Group>
// </Button>
// </Menu.Target>
// <Menu.Dropdown>
// <Menu.Label>{userData?.login}</Menu.Label>
// <Menu.Item
// leftSection={
// colorScheme === 'dark' ? <IconMoon style={{ width: rem(16), height: rem(16) }} stroke={1.5} /> : <IconSun style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
// }
// onClick={() => colorScheme === 'dark' ? setColorScheme('light') : setColorScheme('dark')}
// >
// Тема: {colorScheme === 'dark' ? 'тёмная' : 'светлая'}
// </Menu.Item>
// <Menu.Item
// leftSection={
// <IconSettings style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
// }
// onClick={() => navigate('/settings')}
// >
// Настройки профиля
// </Menu.Item>
// <Menu.Item
// onClick={() => {
// logout()
// navigate("/auth/signin")
// }}
// leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
// >
// Выход
// </Menu.Item>
// <Menu.Item>
// <Flex gap='sm' align='center'>
// <Image src={'/logo2.svg'} w={32} />
// <Text>0.1.0</Text>
// </Flex>
// </Menu.Item>
// </Menu.Dropdown>
// </Menu>
// </Group>
// </Flex>
// </AppShell.Header>
// <AppShell.Navbar style={{ transition: "width 0.2s ease" }}>
// {pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item) => (
// <NavLink
// key={item.path}
// onClick={() => navigate(item.path)}
// label={item.label}
// leftSection={item.icon}
// active={location.pathname === item.path}
// style={{ textWrap: 'nowrap' }}
// // styles={(theme, { active }) => ({
// // root: {
// // color: active ? theme.colors.blue[6] : theme.colors.dark[5],
// // fontWeight: active ? "bold" : "normal",
// // },
// // leftSection: {
// // color: active ? theme.colors.blue[6] : theme.colors.dark[5], // Icon color
// // }
// // })}
// />
// ))}
// </AppShell.Navbar>
// <AppShell.Main>
// <Flex bg={colorScheme === 'dark' ? undefined : '#E8E8E8'} w={{ sm: desktopOpened ? 'calc(100% - 200px)' : 'calc(100% - 50px)', base: '100%' }} h={'calc(100% - 60px)'} style={{ transition: "width 0.2s ease" }} pos={'fixed'}>
// <Outlet />
// </Flex>
// </AppShell.Main>
// </AppShell>
// )
}
export default DashboardLayout

View File

@ -1,10 +1,21 @@
import { Flex } from "@mantine/core";
import { makeStyles } from "@fluentui/react-components";
import { Outlet } from "react-router-dom";
const useStyles = makeStyles({
root: {
display: 'flex',
justifyContent: 'center',
height: '100%',
width: '100%'
}
})
export default function MainLayout() {
const classes = useStyles()
return (
<Flex align='center' justify='center' h='100%' w='100%'>
<div className={classes.root}>
<Outlet />
</Flex>
</div>
)
}