DashboardLayout changes, refactoring, useSWR

This commit is contained in:
cracklesparkle
2024-06-27 17:32:12 +09:00
parent 18fb120777
commit c41e59cd86
19 changed files with 1309 additions and 294 deletions

View File

@ -13,7 +13,9 @@
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@mui/x-charts": "^7.7.1",
"@mui/x-data-grid": "^7.7.1",
"@tanstack/react-query": "^5.45.1",
"autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"postcss": "^8.4.38",
@ -21,6 +23,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1",
"swr": "^2.2.5",
"zod": "^3.23.8",
"zustand": "^4.5.2"
},
@ -2886,6 +2889,44 @@
}
}
},
"node_modules/@mui/x-charts": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.7.1.tgz",
"integrity": "sha512-qUvkGGxBukHXLUqoTUs2rJZz1t+gK0P+bZzQWFbxN93XQyAhrjrOU8VN0x+G7nO3qcXrC2P6s0nevms7ZRSDrA==",
"dependencies": {
"@babel/runtime": "^7.24.7",
"@mui/base": "^5.0.0-beta.40",
"@mui/system": "^5.15.20",
"@mui/utils": "^5.15.20",
"@react-spring/rafz": "^9.7.3",
"@react-spring/web": "^9.7.3",
"clsx": "^2.1.1",
"d3-color": "^3.1.0",
"d3-delaunay": "^6.0.4",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.7.1.tgz",
@ -2965,6 +3006,71 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-spring/animated": {
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz",
"integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==",
"dependencies": {
"@react-spring/shared": "~9.7.3",
"@react-spring/types": "~9.7.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/core": {
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz",
"integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==",
"dependencies": {
"@react-spring/animated": "~9.7.3",
"@react-spring/shared": "~9.7.3",
"@react-spring/types": "~9.7.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-spring/donate"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/rafz": {
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.3.tgz",
"integrity": "sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ=="
},
"node_modules/@react-spring/shared": {
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz",
"integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==",
"dependencies": {
"@react-spring/types": "~9.7.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/types": {
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz",
"integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw=="
},
"node_modules/@react-spring/web": {
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz",
"integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==",
"dependencies": {
"@react-spring/animated": "~9.7.3",
"@react-spring/core": "~9.7.3",
"@react-spring/shared": "~9.7.3",
"@react-spring/types": "~9.7.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
@ -3475,6 +3581,30 @@
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.45.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.45.0.tgz",
"integrity": "sha512-RVfIZQmFUTdjhSAAblvueimfngYyfN6HlwaJUPK71PKd7yi43Vs1S/rdimmZedPWX/WGppcq/U1HOj7O7FwYxw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.45.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.45.1.tgz",
"integrity": "sha512-mYYfJujKg2kxmkRRjA6nn4YKG3ITsKuH22f1kteJ5IuVQqgKUgbaSQfYwVP0gBS05mhwxO03HVpD0t7BMN7WOA==",
"dependencies": {
"@tanstack/query-core": "5.45.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -4209,6 +4339,11 @@
"node": ">= 6"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -4351,6 +4486,111 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
"dependencies": {
"delaunator": "5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@ -4467,6 +4707,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delaunator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
"dependencies": {
"robust-predicates": "^3.0.2"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -5556,6 +5804,14 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@ -7016,6 +7272,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
},
"node_modules/rollup": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
@ -7570,6 +7831,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swr": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
"dependencies": {
"client-only": "^0.0.1",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/tailwindcss": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",

View File

@ -15,7 +15,9 @@
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@mui/x-charts": "^7.7.1",
"@mui/x-data-grid": "^7.7.1",
"@tanstack/react-query": "^5.45.1",
"autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"postcss": "^8.4.38",
@ -23,6 +25,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1",
"swr": "^2.2.5",
"zod": "^3.23.8",
"zustand": "^4.5.2"
},

View File

@ -0,0 +1,96 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Avatar from '@mui/material/Avatar';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import PersonAdd from '@mui/icons-material/PersonAdd';
import Settings from '@mui/icons-material/Settings';
import Logout from '@mui/icons-material/Logout';
export default function AccountMenu() {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<React.Fragment>
<Box sx={{ display: 'flex', alignItems: 'center', textAlign: 'center' }}>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
sx={{ ml: 2 }}
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<Avatar sx={{ width: 32, height: 32 }}></Avatar>
</IconButton>
</Tooltip>
</Box>
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={handleClose}>
<Avatar /> Профиль
</MenuItem>
<Divider />
<MenuItem onClick={handleClose}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Настройки
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Выход
</MenuItem>
</Menu>
</React.Fragment>
);
}

View File

@ -6,9 +6,10 @@ interface Props {
}
export default function DataTable(props: Props) {
return (
<div style={{ flexGrow: 1, height: "100%", width: '100%' }}>
<DataGrid
style={{ width: "100%", height: "100%" }}
rows={props.rows}
columns={props.columns}
initialState={{
@ -18,7 +19,9 @@ export default function DataTable(props: Props) {
}}
pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection
disableRowSelectionOnClick
/>
</div>
);
}

View File

@ -0,0 +1,11 @@
import React from 'react'
const CreateRoleModal = () => {
return (
<div>
</div>
)
}
export default CreateRoleModal

View File

@ -0,0 +1,141 @@
import { Box, Button, Modal, TextField, Typography } from '@mui/material'
import { AxiosResponse } from 'axios';
import { SubmitHandler, useForm } from 'react-hook-form';
import { ApiResponse } from '../../interfaces/auth';
import UserService from '../../services/UserService';
import { CreateUserFormData } from '../../interfaces/user';
interface Props {
open: boolean;
setOpen: (state: boolean) => void;
}
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
display: "flex",
flexDirection: "column",
gap: "8px"
}
export default function CreateUserModal({
open,
setOpen,
}: Props) {
const { register, handleSubmit, formState: { errors } } = useForm<CreateUserFormData>({
defaultValues: {
email: '',
login: '',
phone: '',
name: '',
surname: '',
is_active: true,
password: '',
}
})
const onSubmit: SubmitHandler<CreateUserFormData> = async (data) => {
try {
const response: AxiosResponse<ApiResponse> = await UserService.createUser(data)
console.log(response.data)
} catch (error) {
console.error(error)
}
}
return (
<Modal
open={open}
onClose={() => setOpen(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={style}>
<Typography variant="h6" component="h6" gutterBottom>
Создание пользователя
</Typography>
<TextField
fullWidth
margin="normal"
label="Email"
required
{...register('email', { required: 'Email обязателен' })}
error={!!errors.email}
helperText={errors.email?.message}
/>
<TextField
fullWidth
margin="normal"
label="Логин"
required
{...register('login', { required: 'Логин обязателен' })}
error={!!errors.login}
helperText={errors.login?.message}
/>
<TextField
fullWidth
margin="normal"
label="Телефон"
{...register('phone')}
error={!!errors.phone}
helperText={errors.phone?.message}
/>
<TextField
fullWidth
margin="normal"
label="Имя"
{...register('name')}
error={!!errors.name}
helperText={errors.name?.message}
/>
<TextField
fullWidth
margin="normal"
label="Фамилия"
{...register('surname')}
error={!!errors.surname}
helperText={errors.surname?.message}
/>
<TextField
fullWidth
margin="normal"
type="password"
label="Пароль"
required
{...register('password', { required: 'Пароль обязателен' })}
error={!!errors.password}
helperText={errors.password?.message}
/>
<Box sx={{
display: "flex",
gap: "8px"
}}>
<Button type="submit" variant="contained" color="primary">
Добавить пользователя
</Button>
<Button type="button" variant="outlined" color="primary" onClick={() => setOpen(false)}>
Отмена
</Button>
</Box>
</Box>
</form>
</Modal>
)
}

View File

@ -0,0 +1,23 @@
import useSWR from "swr";
import RoleService from "../services/RoleService";
import UserService from "../services/UserService";
export function useRoles() {
const { data, error, isLoading } = useSWR(`/auth/roles`, RoleService.getRoles)
return {
roles: data?.data,
isLoading,
isError: error
}
}
export function useUsers() {
const { data, error, isLoading } = useSWR(`/auth/user`, UserService.getUsers)
return {
users: data?.data,
isLoading,
isError: error
}
}

View File

@ -16,23 +16,25 @@ export interface UserCreds extends User {
password: string;
}
export interface Role {
name: string;
description?: string | null;
id: number;
}
export interface RoleCreate {
name: string;
description?: string | null;
}
export interface AuthState {
isAuthenticated: boolean;
token: string | null;
userData: UserData | {};
}
export interface SignUpFormData {
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
password: string;
}
export interface SignInFormData {
export interface LoginFormData {
username: string;
password: string;
grant_type: string;

View File

@ -0,0 +1,9 @@
export interface CreateUserFormData {
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
password: string;
}

View File

@ -1,55 +1,78 @@
// Layout for dashboard with responsive drawer
import { Link, NavLink, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuIcon from '@mui/icons-material/Menu';
import MuiDrawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material";
import { getUserData, useAuthStore } from "../store/auth";
import { UserData } from "../interfaces/auth";
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Badge from '@mui/material/Badge';
import Container from '@mui/material/Container';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import NotificationsIcon from '@mui/icons-material/Notifications';
import { AccountCircle, Api, ExitToApp, Home, People, Settings, Shield } from '@mui/icons-material';
import { ListItem, ListItemButton, ListItemIcon, ListItemText, colors } from '@mui/material';
import { Outlet, useNavigate } from 'react-router-dom';
import { UserData } from '../interfaces/auth';
import { getUserData, useAuthStore } from '../store/auth';
import { Theme, useTheme } from '@emotion/react';
import AccountMenu from '../components/AccountMenu';
const drawerWidth = 240;
const drawerWidth: number = 240;
export default function DashboardLayout() {
const authStore = useAuthStore();
const [userData, setUserData] = React.useState<UserData>();
interface AppBarProps extends MuiAppBarProps {
open?: boolean;
}
const location = useLocation()
const AppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== 'open',
})<AppBarProps>(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));
const navigate = useNavigate()
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
({ theme, open }) => ({
'& .MuiDrawer-paper': {
position: 'relative',
whiteSpace: 'nowrap',
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
boxSizing: 'border-box',
...(!open && {
overflowX: 'hidden',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9),
},
}),
},
}),
);
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const pages = [
const pages = [
{
label: "Главная",
path: "/",
@ -70,72 +93,29 @@ export default function DashboardLayout() {
path: "/api-test",
icon: <Api />
},
]
]
const misc = [
{
label: "Настройки",
path: "/settings",
icon: <Settings />
},
{
label: "Выход",
path: "/signOut",
icon: <ExitToApp />
}
]
export default function DashboardLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
const drawer = (
<div>
<Toolbar>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
</Toolbar>
const [open, setOpen] = React.useState(true);
const toggleDrawer = () => {
setOpen(!open);
};
<Divider />
const authStore = useAuthStore();
<List>
{pages.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
const navigate = useNavigate()
<Divider />
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
<List>
{misc.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</div>
);
const [userData, setUserData] = React.useState<UserData>();
React.useEffect(() => {
if (authStore) {
@ -146,87 +126,112 @@ export default function DashboardLayout() {
}
}, [authStore])
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages, ...misc];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
return (
<Box sx={{
display: 'flex',
flexGrow: 1,
height: "100vh"
}}>
<ThemeProvider theme={innerTheme}>
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<AppBar
position="fixed"
<AppBar position="absolute" open={open}>
<Toolbar
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
pr: '24px', // keep right padding when drawer closed
}}
>
<Toolbar>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
onClick={toggleDrawer}
sx={{
marginRight: '36px',
...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
<Typography
component="h1"
variant="h6"
color="inherit"
noWrap
sx={{ flexGrow: 1 }}
>
{getPageTitle()}
</Typography>
<Box sx={{ display: "flex", gap: "8px" }}>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
{/* <IconButton color="inherit">
<Badge badgeContent={0} color="secondary">
<AccountCircle />
</Badge>
</IconButton> */}
<AccountMenu/>
</Box>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
<Drawer variant="permanent" open={open}>
<Toolbar
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
<Box display="flex" justifyContent={'space-between'} width={"100%"}>
<IconButton onClick={toggleDrawer}>
<ChevronLeftIcon />
</IconButton>
</Box>
</Toolbar>
<Divider />
<List component="nav">
{pages.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
style={{ background: location.pathname === item.path ? innerTheme.palette.action.selected : "transparent" }}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</Drawer>
<Box
component="main"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[900],
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` }
maxHeight: "100vh",
overflow: 'auto',
}}
>
<Toolbar />
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Outlet />
</Container>
</Box>
</Box>
</ThemeProvider>
);
}

View File

@ -0,0 +1,252 @@
// Layout for dashboard with responsive drawer
import { Link, NavLink, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material";
import { getUserData, useAuthStore } from "../store/auth";
import { UserData } from "../interfaces/auth";
const drawerWidth = 240;
export default function DashboardLayoutResponsive() {
const [open, setOpen] = React.useState(true);
const authStore = useAuthStore();
const [userData, setUserData] = React.useState<UserData>();
const location = useLocation()
const navigate = useNavigate()
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const pages = [
{
label: "Главная",
path: "/",
icon: <Home />
},
{
label: "Пользователи",
path: "/user",
icon: <People />
},
{
label: "Роли",
path: "/role",
icon: <Shield />
},
{
label: "API Test",
path: "/api-test",
icon: <Api />
},
]
const misc = [
{
label: "Настройки",
path: "/settings",
icon: <Settings />
},
{
label: "Выход",
path: "/signOut",
icon: <ExitToApp />
}
]
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages, ...misc];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
const toggleDrawer = () => {
setOpen(!open);
};
const drawer = (
<div>
<Toolbar>
<Box display="flex" justifyContent={'space-between'} width={"100%"}>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={toggleDrawer}
sx={{
...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
</Box>
</Toolbar>
<Divider />
<List>
{pages.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{misc.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</div >
);
React.useEffect(() => {
if (authStore) {
const stored = getUserData()
if (stored) {
setUserData(stored)
}
}
}, [authStore])
return (
<Box sx={{
display: 'flex',
flexGrow: 1,
height: "100vh"
}}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{getPageTitle()}
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` }
}}
>
<Toolbar />
<Outlet />
</Box>
</Box>
);
}

View File

@ -6,6 +6,9 @@ import { registerSW } from 'virtual:pwa-register'
import { ThemeProvider } from '@emotion/react'
import { createTheme } from '@mui/material'
import { ruRU } from '@mui/material/locale'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient();
const theme = createTheme(
{
@ -30,7 +33,9 @@ const updateSW = registerSW({
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ThemeProvider>
</React.StrictMode>,
)

View File

@ -1,43 +1,32 @@
import { useEffect, useState } from "react"
import { Button } from "@mui/material"
import DataTable from "../components/DataTable"
import { GridColDef } from "@mui/x-data-grid"
import UserService from "../services/UserService"
import { Button, Typography } from "@mui/material"
export default function ApiTest() {
const [users, setUsers] = useState<any>(null)
const [data, setData] = useState<any>(null)
const getUsers = async () => {
await UserService.getUsers().then(response => {
setUsers(response.data)
})
function getRealtimeData(data: any) {
setData(data)
}
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'email', headerName: 'Email', width: 130 },
{ field: 'login', headerName: 'Логин', width: 130 },
{ field: 'phone', headerName: 'Телефон', width: 90 },
{ field: 'name', headerName: 'Имя', width: 90 },
{ field: 'surname', headerName: 'Фамилия', width: 90 },
{ field: 'is_active', headerName: 'Активен', type: "boolean", width: 90 },
{
field: 'role_id',
headerName: 'Роль',
valueGetter: (value, row) => `${value}`,
width: 90
},
];
useEffect(() => {
const sse = new EventSource(`${import.meta.env.VITE_API_SSE_URL}/stream`)
sse.onmessage = e => getRealtimeData(e.data)
sse.onerror = () => {
sse.close()
}
return () => {
sse.close()
}
}, [])
return (
<>
<Button onClick={() => getUsers()}>
Get users
</Button>
{users &&
<DataTable rows={users} columns={columns}/>
}
<Typography>
{JSON.stringify(data)}
</Typography>
</>
)
}

View File

@ -32,7 +32,8 @@ function Roles() {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '16px'
gap: '16px',
flexGrow: 1
}}>
<Button onClick={() => console.log("TODO: Add")}>
Добавить роль

View File

@ -1,43 +1,62 @@
import { useEffect, useState } from "react"
import { Box, Button } from "@mui/material"
import { Box, Button, CircularProgress } from "@mui/material"
import DataTable from "../components/DataTable"
import { GridColDef } from "@mui/x-data-grid"
import UserService from "../services/UserService"
import { useRoles, useUsers } from "../hooks/swrHooks"
import { Role } from "../interfaces/auth"
import { useState } from "react"
import CreateUserModal from "../components/modals/CreateUserModal"
export default function Users() {
const [users, setUsers] = useState<any>(null)
const { users, isError, isLoading } = useUsers()
const getUsers = async () => {
await UserService.getUsers().then(response => {
setUsers(response.data)
})
}
const { roles } = useRoles()
const [open, setOpen] = useState(false)
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'email', headerName: 'Email', width: 130 },
{ field: 'login', headerName: 'Логин', width: 130 },
{ field: 'phone', headerName: 'Телефон', width: 90 },
{ field: 'name', headerName: 'Имя', width: 90 },
{ field: 'surname', headerName: 'Фамилия', width: 90 },
{ field: 'is_active', headerName: 'Активен', type: "boolean", width: 90 },
{ field: 'email', headerName: 'Email', width: 130, editable: true},
{ field: 'login', headerName: 'Логин', width: 130, editable: true },
{ field: 'phone', headerName: 'Телефон', width: 90, editable: true },
{ field: 'name', headerName: 'Имя', width: 90, editable: true },
{ field: 'surname', headerName: 'Фамилия', width: 90, editable: true },
{ field: 'is_active', headerName: 'Активен', type: "boolean", width: 90, editable: true},
{
field: 'role_id',
headerName: 'Роль',
valueGetter: (value, row) => `${value}`,
valueGetter: (value, row) => {
if (roles) {
const roleName = roles.find((role: Role) => role.id === value).name
return roleName
} else {
return value
}
},
width: 90
},
];
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <CircularProgress />
return (
<Box>
<Button onClick={() => getUsers()}>
Get users
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "16px",
}}
>
<Button onClick={() => setOpen(true)}>
Добавить пользователя
</Button>
{users &&
<DataTable rows={users} columns={columns}/>
}
<CreateUserModal
open={open}
setOpen={setOpen}
/>
<DataTable rows={users} columns={columns} />
</Box>
)
}

View File

@ -1,14 +1,14 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box } from '@mui/material';
import { AxiosResponse } from 'axios';
import { SignInFormData, ApiResponse } from '../../interfaces/auth';
import { ApiResponse, LoginFormData } from '../../interfaces/auth';
import { login, setUserData } from '../../store/auth';
import { useNavigate } from 'react-router-dom';
import AuthService from '../../services/AuthService';
import UserService from '../../services/UserService';
const SignIn = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SignInFormData>({
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormData>({
defaultValues: {
username: '',
password: '',
@ -21,10 +21,10 @@ const SignIn = () => {
const navigate = useNavigate();
const onSubmit: SubmitHandler<SignInFormData> = async (data) => {
const onSubmit: SubmitHandler<LoginFormData> = async (data) => {
const formBody = new URLSearchParams();
for (const key in data) {
formBody.append(key, data[key as keyof SignInFormData] as string);
formBody.append(key, data[key as keyof LoginFormData] as string);
}
try {

View File

@ -1,12 +1,12 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box } from '@mui/material';
import axios, { AxiosResponse } from 'axios';
import { SignUpFormData, ApiResponse } from '../../interfaces/auth';
import axiosInstance from '../../http/axiosInstance';
import { AxiosResponse } from 'axios';
import { ApiResponse } from '../../interfaces/auth';
import UserService from '../../services/UserService';
import { CreateUserFormData } from '../../interfaces/user';
const SignUp = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SignUpFormData>({
const { register, handleSubmit, formState: { errors } } = useForm<CreateUserFormData>({
defaultValues: {
email: '',
login: '',
@ -19,7 +19,7 @@ const SignUp = () => {
})
const onSubmit: SubmitHandler<SignUpFormData> = async (data) => {
const onSubmit: SubmitHandler<CreateUserFormData> = async (data) => {
try {
const response: AxiosResponse<ApiResponse> = await UserService.createUser(data)
console.log('Успешная регистрация:', response.data);

View File

@ -1,11 +1,21 @@
import axiosInstance from "../http/axiosInstance";
import { Role, RoleCreate } from "../interfaces/auth";
export default class RoleService {
static async getRoles() {
return await axiosInstance.get(`/auth/roles`)
}
static async createRole(data: RoleCreate) {
return await axiosInstance.post(`/auth/roles/`, data)
}
static async getRoleById(id: number) {
return await axiosInstance.get(`/auth/roles/${id}`)
}
// static async deleteRole(id: number) {
// return await axiosInstance.delete(`/auth/roles/${id}`)
// }
}

View File

@ -1022,7 +1022,7 @@
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz"
integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.11.4", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0":
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.11.4", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0":
version "11.11.4"
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz"
integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==
@ -1052,7 +1052,7 @@
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz"
integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==
"@emotion/styled@^11.11.5", "@emotion/styled@^11.3.0":
"@emotion/styled@^11.11.5", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1":
version "11.11.5"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz"
integrity sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==
@ -1219,7 +1219,7 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@mui/base@5.0.0-beta.40":
"@mui/base@^5.0.0-beta.40", "@mui/base@5.0.0-beta.40":
version "5.0.0-beta.40"
resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz"
integrity sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==
@ -1310,6 +1310,25 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/x-charts@^7.7.1":
version "7.7.1"
resolved "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.7.1.tgz"
integrity sha512-qUvkGGxBukHXLUqoTUs2rJZz1t+gK0P+bZzQWFbxN93XQyAhrjrOU8VN0x+G7nO3qcXrC2P6s0nevms7ZRSDrA==
dependencies:
"@babel/runtime" "^7.24.7"
"@mui/base" "^5.0.0-beta.40"
"@mui/system" "^5.15.20"
"@mui/utils" "^5.15.20"
"@react-spring/rafz" "^9.7.3"
"@react-spring/web" "^9.7.3"
clsx "^2.1.1"
d3-color "^3.1.0"
d3-delaunay "^6.0.4"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-shape "^3.2.0"
prop-types "^15.8.1"
"@mui/x-data-grid@^7.7.1":
version "7.7.1"
resolved "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.7.1.tgz"
@ -1353,6 +1372,50 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@react-spring/animated@~9.7.3":
version "9.7.3"
resolved "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz"
integrity sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==
dependencies:
"@react-spring/shared" "~9.7.3"
"@react-spring/types" "~9.7.3"
"@react-spring/core@~9.7.3":
version "9.7.3"
resolved "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz"
integrity sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==
dependencies:
"@react-spring/animated" "~9.7.3"
"@react-spring/shared" "~9.7.3"
"@react-spring/types" "~9.7.3"
"@react-spring/rafz@^9.7.3":
version "9.7.3"
resolved "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.3.tgz"
integrity sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ==
"@react-spring/shared@~9.7.3":
version "9.7.3"
resolved "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz"
integrity sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==
dependencies:
"@react-spring/types" "~9.7.3"
"@react-spring/types@~9.7.3":
version "9.7.3"
resolved "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz"
integrity sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==
"@react-spring/web@^9.7.3":
version "9.7.3"
resolved "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz"
integrity sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==
dependencies:
"@react-spring/animated" "~9.7.3"
"@react-spring/core" "~9.7.3"
"@react-spring/shared" "~9.7.3"
"@react-spring/types" "~9.7.3"
"@remix-run/router@1.16.1":
version "1.16.1"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz"
@ -1464,6 +1527,18 @@
dependencies:
"@swc/counter" "^0.1.3"
"@tanstack/query-core@5.45.0":
version "5.45.0"
resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.45.0.tgz"
integrity sha512-RVfIZQmFUTdjhSAAblvueimfngYyfN6HlwaJUPK71PKd7yi43Vs1S/rdimmZedPWX/WGppcq/U1HOj7O7FwYxw==
"@tanstack/react-query@^5.45.1":
version "5.45.1"
resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.45.1.tgz"
integrity sha512-mYYfJujKg2kxmkRRjA6nn4YKG3ITsKuH22f1kteJ5IuVQqgKUgbaSQfYwVP0gBS05mhwxO03HVpD0t7BMN7WOA==
dependencies:
"@tanstack/query-core" "5.45.0"
"@types/estree@^1.0.0", "@types/estree@1.0.5":
version "1.0.5"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
@ -1904,6 +1979,11 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@^2.1.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
@ -2012,6 +2092,74 @@ csstype@^3.0.2, csstype@^3.1.3:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
"d3-array@2 - 3", "d3-array@2.10.0 - 3":
version "3.2.4"
resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
d3-color@^3.1.0, "d3-color@1 - 3":
version "3.1.0"
resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
d3-delaunay@^6.0.4:
version "6.0.4"
resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz"
integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
dependencies:
delaunator "5"
"d3-format@1 - 3":
version "3.1.0"
resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
d3-interpolate@^3.0.1, "d3-interpolate@1.2.0 - 3":
version "3.0.1"
resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
d3-shape@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
"d3-time@1 - 3", "d3-time@2.1.1 - 3":
version "3.1.0"
resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
data-view-buffer@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz"
@ -2074,6 +2222,13 @@ define-properties@^1.2.0, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
delaunator@5:
version "5.0.1"
resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz"
integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
dependencies:
robust-predicates "^3.0.2"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
@ -2759,6 +2914,11 @@ internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
is-array-buffer@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz"
@ -3463,7 +3623,7 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
"react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0:
"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@ -3516,7 +3676,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
"react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0", react@^18.2.0, react@^18.3.1, react@>=16.6.0, react@>=16.8, react@>=16.8.0:
"react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0", react@^18.0.0, react@^18.2.0, react@^18.3.1, react@>=16.6.0, react@>=16.8, react@>=16.8.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@ -3626,6 +3786,11 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
robust-predicates@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz"
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
"rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.43.1:
version "2.79.1"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz"
@ -3957,6 +4122,14 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.2.5:
version "2.2.5"
resolved "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz"
integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==
dependencies:
client-only "^0.0.1"
use-sync-external-store "^1.2.0"
tailwindcss@^3.4.4:
version "3.4.4"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz"
@ -4189,7 +4362,7 @@ uri-js@^4.2.2, uri-js@^4.4.1:
dependencies:
punycode "^2.1.0"
use-sync-external-store@1.2.0:
use-sync-external-store@^1.2.0, use-sync-external-store@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==