Layout, Pages, Dashboard, MUI

This commit is contained in:
cracklesparkle
2024-06-20 16:59:59 +09:00
parent d53694a0b3
commit d6906503d1
27 changed files with 8076 additions and 142 deletions

View File

@ -0,0 +1,2 @@
VITE_API_URL=
VITE_API_AUTH_URL=

View File

@ -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

View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }
} }

View File

@ -1,20 +1,29 @@
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 index path="/" element={ <Main />} /> <Route element={<MainLayout/>}>
<Route path="/auth/signin" element={<SignIn/>}/>
</Route>
<Route element={<DashboardLayout />}>
<Route path="/" element={<Main />} />
<Route path="/user" element={<Users />} /> <Route path="/user" element={<Users />} />
<Route path="/role" element={<Roles />} /> <Route path="/role" element={<Roles />} />
<Route path="/api-test" element={<ApiTest />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Route>
</Routes> </Routes>
</Router> </Router>
</> </>

View File

@ -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>
</>
)
}

View File

@ -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>
);
}

View 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>
);
}

View File

View 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>
);
}

View File

@ -0,0 +1,11 @@
// Layout for fullscreen pages
import { Outlet } from "react-router-dom";
export default function MainLayout() {
return (
<>
<Outlet/>
</>
)
}

View File

@ -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>,

View 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>
</>
)
}

View File

@ -1,3 +1,7 @@
export default function Main() { export default function Main() {
return ( <><h1>Main page</h1></>) return (
<>
Главная
</>
)
} }

View File

@ -1,3 +1,7 @@
export default function NotFound() { export default function NotFound() {
return ( <><h1>Page not found</h1></>) return (
<>
<h1>Page not found</h1>
</>
)
} }

View File

@ -10,9 +10,9 @@ interface IRoleCard{
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>
@ -23,4 +23,4 @@ function Users() {
) )
} }
export default Users export default Roles

View File

@ -7,7 +7,7 @@ interface ICard{
} }
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} />)}

View File

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

View 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`)
}
}

View 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)
})
}
}

View File

View 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"],

View File

@ -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