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", "@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20", "@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20", "@mui/material": "^5.15.20",
"@mui/x-charts": "^7.7.1",
"@mui/x-data-grid": "^7.7.1", "@mui/x-data-grid": "^7.7.1",
"@tanstack/react-query": "^5.45.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"axios": "^1.7.2", "axios": "^1.7.2",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@ -21,6 +23,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.52.0", "react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"swr": "^2.2.5",
"zod": "^3.23.8", "zod": "^3.23.8",
"zustand": "^4.5.2" "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": { "node_modules/@mui/x-data-grid": {
"version": "7.7.1", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.7.1.tgz", "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" "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": { "node_modules/@remix-run/router": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
@ -3475,6 +3581,30 @@
"@swc/counter": "^0.1.3" "@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": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -4209,6 +4339,11 @@
"node": ">= 6" "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": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "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", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "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": { "node_modules/data-view-buffer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", "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" "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -5556,6 +5804,14 @@
"node": ">= 0.4" "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": { "node_modules/is-array-buffer": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", "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" "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": { "node_modules/rollup": {
"version": "4.18.0", "version": "4.18.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
@ -7570,6 +7831,18 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/tailwindcss": {
"version": "3.4.4", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",

View File

@ -15,7 +15,9 @@
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20", "@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20", "@mui/material": "^5.15.20",
"@mui/x-charts": "^7.7.1",
"@mui/x-data-grid": "^7.7.1", "@mui/x-data-grid": "^7.7.1",
"@tanstack/react-query": "^5.45.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"axios": "^1.7.2", "axios": "^1.7.2",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@ -23,6 +25,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.52.0", "react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"swr": "^2.2.5",
"zod": "^3.23.8", "zod": "^3.23.8",
"zustand": "^4.5.2" "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) { export default function DataTable(props: Props) {
return ( return (
<div style={{ flexGrow: 1, height: "100%", width: '100%' }}>
<DataGrid <DataGrid
style={{ width: "100%", height: "100%" }}
rows={props.rows} rows={props.rows}
columns={props.columns} columns={props.columns}
initialState={{ initialState={{
@ -18,7 +19,9 @@ export default function DataTable(props: Props) {
}} }}
pageSizeOptions={[10, 20, 50, 100]} pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection 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; password: string;
} }
export interface Role {
name: string;
description?: string | null;
id: number;
}
export interface RoleCreate {
name: string;
description?: string | null;
}
export interface AuthState { export interface AuthState {
isAuthenticated: boolean; isAuthenticated: boolean;
token: string | null; token: string | null;
userData: UserData | {}; userData: UserData | {};
} }
export interface SignUpFormData { export interface LoginFormData {
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
password: string;
}
export interface SignInFormData {
username: string; username: string;
password: string; password: string;
grant_type: 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,53 +1,76 @@
// Layout for dashboard with responsive drawer
import { Link, NavLink, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"
import * as React from 'react'; import * as React from 'react';
import AppBar from '@mui/material/AppBar'; import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider'; import MuiDrawer from '@mui/material/Drawer';
import Drawer from '@mui/material/Drawer'; import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton'; import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar';
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 Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material"; import Divider from '@mui/material/Divider';
import { getUserData, useAuthStore } from "../store/auth"; import IconButton from '@mui/material/IconButton';
import { UserData } from "../interfaces/auth"; 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() { interface AppBarProps extends MuiAppBarProps {
const authStore = useAuthStore(); open?: boolean;
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 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 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 pages = [ const pages = [
{ {
@ -72,70 +95,27 @@ export default function DashboardLayout() {
}, },
] ]
const misc = [ export default function DashboardLayout() {
{ const theme = useTheme()
label: "Настройки", const innerTheme = createTheme(theme)
path: "/settings",
icon: <Settings />
},
{
label: "Выход",
path: "/signOut",
icon: <ExitToApp />
}
]
const drawer = ( const [open, setOpen] = React.useState(true);
<div> const toggleDrawer = () => {
<Toolbar> setOpen(!open);
<Box> };
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
</Toolbar>
<Divider /> const authStore = useAuthStore();
<List> const navigate = useNavigate()
{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 /> const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
<List> const [userData, setUserData] = React.useState<UserData>();
{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(() => { React.useEffect(() => {
if (authStore) { if (authStore) {
@ -146,87 +126,112 @@ export default function DashboardLayout() {
} }
}, [authStore]) }, [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 ( return (
<Box sx={{ <ThemeProvider theme={innerTheme}>
display: 'flex', <Box sx={{ display: 'flex' }}>
flexGrow: 1,
height: "100vh"
}}>
<CssBaseline /> <CssBaseline />
<AppBar <AppBar position="absolute" open={open}>
position="fixed" <Toolbar
sx={{ sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` }, pr: '24px', // keep right padding when drawer closed
ml: { sm: `${drawerWidth}px` },
}} }}
> >
<Toolbar>
<IconButton <IconButton
edge="start"
color="inherit" color="inherit"
aria-label="open drawer" aria-label="open drawer"
edge="start" onClick={toggleDrawer}
onClick={handleDrawerToggle} sx={{
sx={{ mr: 2, display: { sm: 'none' } }} marginRight: '36px',
...(open && { display: 'none' }),
}}
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" noWrap component="div">
<Typography
component="h1"
variant="h6"
color="inherit"
noWrap
sx={{ flexGrow: 1 }}
>
{getPageTitle()} {getPageTitle()}
</Typography> </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> </Toolbar>
</AppBar> </AppBar>
<Drawer variant="permanent" open={open}>
<Box <Toolbar
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={{ sx={{
display: { xs: 'block', sm: 'none' }, display: 'flex',
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}} }}
> >
{drawer} <Box display="flex" justifyContent={'space-between'} width={"100%"}>
</Drawer> <IconButton onClick={toggleDrawer}>
<Drawer <ChevronLeftIcon />
variant="permanent" </IconButton>
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box> </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 <Box
component="main" component="main"
sx={{ sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[900],
flexGrow: 1, flexGrow: 1,
p: 3, maxHeight: "100vh",
width: { sm: `calc(100% - ${drawerWidth}px)` } overflow: 'auto',
}} }}
> >
<Toolbar /> <Toolbar />
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Outlet /> <Outlet />
</Container>
</Box> </Box>
</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 { ThemeProvider } from '@emotion/react'
import { createTheme } from '@mui/material' import { createTheme } from '@mui/material'
import { ruRU } from '@mui/material/locale' import { ruRU } from '@mui/material/locale'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient();
const theme = createTheme( const theme = createTheme(
{ {
@ -30,7 +33,9 @@ const updateSW = registerSW({
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<App /> <App />
</QueryClientProvider>
</ThemeProvider> </ThemeProvider>
</React.StrictMode>, </React.StrictMode>,
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,21 @@
import axiosInstance from "../http/axiosInstance"; import axiosInstance from "../http/axiosInstance";
import { Role, RoleCreate } from "../interfaces/auth";
export default class RoleService { export default class RoleService {
static async getRoles() { static async getRoles() {
return await axiosInstance.get(`/auth/roles`) return await axiosInstance.get(`/auth/roles`)
} }
static async createRole(data: RoleCreate) {
return await axiosInstance.post(`/auth/roles/`, data)
}
static async getRoleById(id: number) { static async getRoleById(id: number) {
return await axiosInstance.get(`/auth/roles/${id}`) 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" resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz"
integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== 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" version "11.11.4"
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz" resolved "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz"
integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==
@ -1052,7 +1052,7 @@
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz" resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz"
integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== 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" version "11.11.5"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz" resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz"
integrity sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ== integrity sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==
@ -1219,7 +1219,7 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@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" version "5.0.0-beta.40"
resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz" resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz"
integrity sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ== integrity sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==
@ -1310,6 +1310,25 @@
prop-types "^15.8.1" prop-types "^15.8.1"
react-is "^18.2.0" 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": "@mui/x-data-grid@^7.7.1":
version "7.7.1" version "7.7.1"
resolved "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.7.1.tgz" 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" resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== 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": "@remix-run/router@1.16.1":
version "1.16.1" version "1.16.1"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz" resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz"
@ -1464,6 +1527,18 @@
dependencies: dependencies:
"@swc/counter" "^0.1.3" "@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": "@types/estree@^1.0.0", "@types/estree@1.0.5":
version "1.0.5" version "1.0.5"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
@ -1904,6 +1979,11 @@ chokidar@^3.5.3:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" 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: clsx@^2.1.0, clsx@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" 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" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== 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: data-view-buffer@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz" 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" has-property-descriptors "^1.0.0"
object-keys "^1.1.1" 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: delayed-stream@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" 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" hasown "^2.0.0"
side-channel "^1.0.4" 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: is-array-buffer@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz" resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz"
@ -3463,7 +3623,7 @@ randombytes@^2.1.0:
dependencies: dependencies:
safe-buffer "^5.1.0" 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" version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@ -3516,7 +3676,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" 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" version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@ -3626,6 +3786,11 @@ rimraf@^3.0.2:
dependencies: dependencies:
glob "^7.1.3" 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: "rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.43.1:
version "2.79.1" version "2.79.1"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz" 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" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 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: tailwindcss@^3.4.4:
version "3.4.4" version "3.4.4"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz" 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: dependencies:
punycode "^2.1.0" 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" version "1.2.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==