forked from VinokurovVE/tests
Layout, Pages, Dashboard, MUI
This commit is contained in:
2
frontend_reactjs/.env.example
Normal file
2
frontend_reactjs/.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=
|
||||||
|
VITE_API_AUTH_URL=
|
@ -1,30 +1,18 @@
|
|||||||
# React + TypeScript + Vite
|
# Experimental Frontend
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
## Структура проекта
|
||||||
|
- `src/assets/` - Статические ассеты
|
||||||
|
- `src/components/` - Компоненты
|
||||||
|
- `src/constants/` - Константы
|
||||||
|
- `src/layouts/` - Макеты для разных частей, пока есть MainLayout, используемый всеми роутами
|
||||||
|
- `src/pages/` - Страницы
|
||||||
|
- `src/services/` - сервисы / API
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
## UI
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
В основном, используется Material UI https://mui.com/material-ui
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
Для кастомных компонентов следует создать директорию в `src/components/НазваниеКомпонента` со стилями, если необходимо
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
## Env vars
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
`.env.example` должен описывать используемые переменные, в работе же используется `.env.local` или `.env`
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default {
|
|
||||||
// other rules...
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
|
||||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
4721
frontend_reactjs/package-lock.json
generated
4721
frontend_reactjs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,13 +10,20 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"-": "^0.0.1",
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/styled": "^11.11.5",
|
||||||
|
"@mui/icons-material": "^5.15.20",
|
||||||
|
"@mui/material": "^5.15.20",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.5",
|
"react-hook-form": "^7.51.5",
|
||||||
"react-router-dom": "^6.23.1"
|
"react-router-dom": "^6.23.1",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
@ -29,6 +36,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0",
|
||||||
|
"vite-plugin-pwa": "^0.20.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
import { BrowserRouter as Router, Routes,Route} from "react-router-dom"
|
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
|
||||||
|
import Main from "./pages/Main"
|
||||||
import Users from "./pages/Users"
|
import Users from "./pages/Users"
|
||||||
import Roles from "./pages/Roles"
|
import Roles from "./pages/Roles"
|
||||||
import Main from "./pages/Main"
|
|
||||||
import NotFound from "./pages/NotFound"
|
import NotFound from "./pages/NotFound"
|
||||||
import Navigate from "./components/Navigate"
|
import DashboardLayout from "./layouts/DashboardLayout"
|
||||||
|
import MainLayout from "./layouts/MainLayout"
|
||||||
|
import SignIn from "./pages/auth/SignIn"
|
||||||
|
import ApiTest from "./pages/ApiTest"
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navigate />
|
<Router>
|
||||||
<Router>
|
<Routes>
|
||||||
<Routes>
|
<Route element={<MainLayout/>}>
|
||||||
<Route index path="/" element={ <Main />} />
|
<Route path="/auth/signin" element={<SignIn/>}/>
|
||||||
<Route path="/user" element={ <Users />} />
|
</Route>
|
||||||
<Route path="/role" element={ <Roles />} />
|
|
||||||
<Route path="*" element={<NotFound />}/>
|
<Route element={<DashboardLayout />}>
|
||||||
</Routes>
|
<Route path="/" element={<Main />} />
|
||||||
</Router>
|
<Route path="/user" element={<Users />} />
|
||||||
|
<Route path="/role" element={<Roles />} />
|
||||||
|
<Route path="/api-test" element={<ApiTest />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
export function useDataFetching<T> (url:string, initData:T):T{
|
export function useDataFetching<T>(url: string, initData: T): T {
|
||||||
const [data, setData] = useState<T>(initData);
|
const [data, setData] = useState<T>(initData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
export default function Navigate(){
|
|
||||||
return(
|
|
||||||
<>
|
|
||||||
<nav className="bg-white border-gray-200 dark:bg-gray-900">
|
|
||||||
<div className="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
|
||||||
<ul className="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
|
|
||||||
<li>
|
|
||||||
<a href="/" className="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Главная</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/user" className="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Пользователи</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/role" className="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Роли</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,171 @@
|
|||||||
|
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 InboxIcon from '@mui/icons-material/MoveToInbox';
|
||||||
|
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 MailIcon from '@mui/icons-material/Mail';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
export default function ResponsiveDrawer() {
|
||||||
|
//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 drawer = (
|
||||||
|
<div>
|
||||||
|
<Toolbar />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{['All mail', 'Trash', 'Spam'].map((text, index) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<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">
|
||||||
|
Dashboard
|
||||||
|
</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 />
|
||||||
|
<Typography paragraph>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||||
|
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
|
||||||
|
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
|
||||||
|
imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus.
|
||||||
|
Convallis convallis tellus id interdum velit laoreet id donec ultrices.
|
||||||
|
Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit
|
||||||
|
adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra
|
||||||
|
nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum
|
||||||
|
leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis
|
||||||
|
feugiat vivamus at augue. At augue eget arcu dictum varius duis at
|
||||||
|
consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa
|
||||||
|
sapien faucibus et molestie ac.
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Consequat mauris nunc congue nisi vitae suscipit. Fringilla est ullamcorper
|
||||||
|
eget nulla facilisi etiam dignissim diam. Pulvinar elementum integer enim
|
||||||
|
neque volutpat ac tincidunt. Ornare suspendisse sed nisi lacus sed viverra
|
||||||
|
tellus. Purus sit amet volutpat consequat mauris. Elementum eu facilisis
|
||||||
|
sed odio morbi. Euismod lacinia at quis risus sed vulputate odio. Morbi
|
||||||
|
tincidunt ornare massa eget egestas purus viverra accumsan in. In hendrerit
|
||||||
|
gravida rutrum quisque non tellus orci ac. Pellentesque nec nam aliquam sem
|
||||||
|
et tortor. Habitant morbi tristique senectus et. Adipiscing elit duis
|
||||||
|
tristique sollicitudin nibh sit. Ornare aenean euismod elementum nisi quis
|
||||||
|
eleifend. Commodo viverra maecenas accumsan lacus vel facilisis. Nulla
|
||||||
|
posuere sollicitudin aliquam ultrices sagittis orci a.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
30
frontend_reactjs/src/components/navigation/NavTabs.tsx
Normal file
30
frontend_reactjs/src/components/navigation/NavTabs.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Tab, Tabs } from "@mui/material"
|
||||||
|
import { Link, matchPath, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
function useRouteMatch(patterns: readonly string[]) {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
for (let i = 0; i < patterns.length; i += 1) {
|
||||||
|
const pattern = patterns[i]
|
||||||
|
const possibleMatch = matchPath(pattern, pathname)
|
||||||
|
if (possibleMatch !== null) {
|
||||||
|
return possibleMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavTabs() {
|
||||||
|
const routeMatch = useRouteMatch(['/', '/user', '/role']);
|
||||||
|
const currentTab = routeMatch?.pattern?.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs value={currentTab}>
|
||||||
|
<Tab label="Главная" value="/" to="/" component={Link} />
|
||||||
|
<Tab label="Пользователи" value="/user" to="/user" component={Link} />
|
||||||
|
<Tab label="Роли" value="/role" to="/role" component={Link} />
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
0
frontend_reactjs/src/constants/index.ts
Normal file
0
frontend_reactjs/src/constants/index.ts
Normal file
183
frontend_reactjs/src/layouts/DashboardLayout.tsx
Normal file
183
frontend_reactjs/src/layouts/DashboardLayout.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// Layout for dashboard with responsive drawer
|
||||||
|
|
||||||
|
import { Outlet } 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";
|
||||||
|
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
export default function DashboardLayout() {
|
||||||
|
//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 drawer = (
|
||||||
|
<div>
|
||||||
|
<Toolbar />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{pages.map((item, index) => (
|
||||||
|
<ListItem key={index} disablePadding>
|
||||||
|
<ListItemButton href={item.path}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{item.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={item.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{misc.map((item, index) => (
|
||||||
|
<ListItem key={index} disablePadding>
|
||||||
|
<ListItemButton href={item.path}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{item.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={item.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<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">
|
||||||
|
Dashboard
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
11
frontend_reactjs/src/layouts/MainLayout.tsx
Normal file
11
frontend_reactjs/src/layouts/MainLayout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Layout for fullscreen pages
|
||||||
|
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function MainLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Outlet/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -2,8 +2,20 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
const updateSW = registerSW({
|
||||||
|
onNeedRefresh() {
|
||||||
|
if (confirm("New content available. Reload?")) {
|
||||||
|
updateSW(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOfflineReady() {
|
||||||
|
console.log("offline ready");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
23
frontend_reactjs/src/pages/ApiTest.tsx
Normal file
23
frontend_reactjs/src/pages/ApiTest.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import UserService from "../services/UserService"
|
||||||
|
import AuthService from "../services/AuthService"
|
||||||
|
import { Button } from "@mui/material"
|
||||||
|
|
||||||
|
export default function ApiTest() {
|
||||||
|
const [temp, setTemp] = useState<any>(null)
|
||||||
|
|
||||||
|
const hello = async () => {
|
||||||
|
await AuthService.hello().then(response => {
|
||||||
|
setTemp(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>{JSON.stringify(temp)}</div>
|
||||||
|
<Button onClick={() => hello()}>
|
||||||
|
Hello
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
export default function Main (){
|
export default function Main() {
|
||||||
return ( <><h1>Main page</h1></>)
|
return (
|
||||||
|
<>
|
||||||
|
Главная
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,3 +1,7 @@
|
|||||||
export default function NotFound (){
|
export default function NotFound() {
|
||||||
return ( <><h1>Page not found</h1></>)
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,26 +1,26 @@
|
|||||||
import { useState} from 'react'
|
import { useState } from 'react'
|
||||||
import RoleCard from '../components/RoleCard'
|
import RoleCard from '../components/RoleCard'
|
||||||
import Modal from '../components/Modal'
|
import Modal from '../components/Modal'
|
||||||
import useDataFetching from '../components/FetchingData'
|
import useDataFetching from '../components/FetchingData'
|
||||||
|
|
||||||
interface IRoleCard{
|
interface IRoleCard {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
interface Props {
|
interface Props {
|
||||||
showModal: boolean;
|
showModal: boolean;
|
||||||
}
|
}
|
||||||
function Users() {
|
function Roles() {
|
||||||
const [showModal, setShowModal] = useState<Props>({showModal: false});
|
const [showModal, setShowModal] = useState<Props>({ showModal: false });
|
||||||
const cards = useDataFetching<IRoleCard[]>("http://localhost:8000/auth/role/",[])
|
const cards = useDataFetching<IRoleCard[]>(`${import.meta.env.VITE_API_URL}/auth/role/`, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{cards.map((card, index) => <RoleCard key={index} {...card}/>)}
|
{cards.map((card, index) => <RoleCard key={index} {...card} />)}
|
||||||
<button className='absolute w-0 h-0' onClick={() => setShowModal({showModal:true})}>+</button>
|
<button className='absolute w-0 h-0' onClick={() => setShowModal({ showModal: true })}>+</button>
|
||||||
<Modal {...showModal}/>
|
<Modal {...showModal} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Users
|
export default Roles
|
@ -1,16 +1,16 @@
|
|||||||
import Card from '../components/Card'
|
import Card from '../components/Card'
|
||||||
import useDataFetching from '../components/FetchingData'
|
import useDataFetching from '../components/FetchingData'
|
||||||
interface ICard{
|
interface ICard {
|
||||||
firstname: string
|
firstname: string
|
||||||
lastname: string
|
lastname: string
|
||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function Users() {
|
function Users() {
|
||||||
const cards= useDataFetching<ICard[]>("http://localhost:8000/auth/user/",[])
|
const cards = useDataFetching<ICard[]>(`${import.meta.env.VITE_API_URL}/auth/user/`, [])
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{cards.map((card, index) => <Card key={index} {...card}/>)}
|
{cards.map((card, index) => <Card key={index} {...card} />)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
9
frontend_reactjs/src/pages/auth/SignIn.tsx
Normal file
9
frontend_reactjs/src/pages/auth/SignIn.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const SignIn = () => {
|
||||||
|
return (
|
||||||
|
<div>SignIn</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignIn
|
0
frontend_reactjs/src/pages/auth/SignUp.tsx
Normal file
0
frontend_reactjs/src/pages/auth/SignUp.tsx
Normal file
7
frontend_reactjs/src/services/AuthService.ts
Normal file
7
frontend_reactjs/src/services/AuthService.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default class AuthService {
|
||||||
|
static async hello() {
|
||||||
|
return await axios.get(`${import.meta.env.VITE_API_AUTH_URL}/hello`)
|
||||||
|
}
|
||||||
|
}
|
29
frontend_reactjs/src/services/UserService.ts
Normal file
29
frontend_reactjs/src/services/UserService.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Data mockup
|
||||||
|
let users =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"email": "string",
|
||||||
|
"login": "string",
|
||||||
|
"phone": "string",
|
||||||
|
"name": "string",
|
||||||
|
"surname": "string",
|
||||||
|
"is_active": true,
|
||||||
|
"id": 0,
|
||||||
|
"role_id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default class UserService {
|
||||||
|
static async getUsers() {
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!users) {
|
||||||
|
return setTimeout(
|
||||||
|
() => reject(new Error('Users not found')),
|
||||||
|
250
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => resolve(users), 250)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
0
frontend_reactjs/src/store/auth-store.ts
Normal file
0
frontend_reactjs/src/store/auth-store.ts
Normal file
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"vite-plugin-pwa/client"
|
||||||
|
],
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
@ -1,7 +1,78 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ["**/*"],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: ({ request }) => request.mode === 'navigate',
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'html-cache',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /\.(?:js|css)$/,
|
||||||
|
handler: 'StaleWhileRevalidate',
|
||||||
|
options: {
|
||||||
|
cacheName: 'static-resources',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'image-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 50,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
includeAssets: [
|
||||||
|
"**/*",
|
||||||
|
],
|
||||||
|
manifest: {
|
||||||
|
"theme_color": "#f69435",
|
||||||
|
"background_color": "#f69435",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"start_url": "/",
|
||||||
|
"short_name": "Vite PWA",
|
||||||
|
"description": "Vite PWA Boilerplate",
|
||||||
|
"name": "Vite PWA Boilerplate",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-256x256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user