Compare commits

...

No commits in common. "main" and "testing" have entirely different histories.

80 changed files with 9240 additions and 39 deletions

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>OSGOS Admin</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

3734
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,16 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.0.8",
"axios": "^1.6.7",
"dotenv": "^16.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.22.0",
"styled-components": "^6.1.8" "styled-components": "^6.1.8"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.55", "@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
@ -24,6 +28,7 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.1.0" "vite": "^5.1.0",
"vite-plugin-pwa": "^0.19.0"
} }
} }

View File

@ -1,7 +1,18 @@
import React from 'react' import { Route, Routes } from 'react-router-dom'
import Login from './pages/Login'
import Registrations from './pages/Registrations/Registrations'
import Repository from './pages/Repository/Repository'
import Store from './pages/Store/Store'
export default function App() { export default function App() {
return ( return (
<div>App</div> <>
<Routes>
<Route path={'/registrations'} element={<Registrations />} />
<Route path={'/repository'} element={<Repository />} />
<Route path={'/store'} element={<Store />} />
<Route path={'/login'} element={<Login />} />
</Routes>
</>
) )
} }

View File

@ -0,0 +1,40 @@
import { createGlobalStyle } from 'styled-components'
export const GlobalStyles = createGlobalStyle`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Roboto";
color: ${props => props.theme.textColor};
background: ${props => props.theme.bg};
position: relative;
min-height: 100vh;
}
a {
text-decoration: none;
color: ${props => props.theme.textColor};
}
h1 {
${props => props.theme.h1}
}
h2 {
${props => props.theme.h2}
}
h3 {
${props => props.theme.h3}
}
p {
${props => props.theme.paragraph}
}
`

34
src/assets/theme.ts Normal file
View File

@ -0,0 +1,34 @@
export const theme = {
// color
primary: '#006666',
secondary: '#003333',
bg: '#F5F5F5',
textColor: '#292929',
focusColor: '#EDEDED',
// typography
h1: `
font-weight: 700;
font-size: 36px;
`,
h2: `
font-weight: 600;
font-size: 24px;
`,
h3: `
font-weight: 600;
font-size: 18px;
`,
paragraph: `
font-weight: 400;
font-size: 16px;
`,
// shadows
mainShadow: `
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
`,
innerShadow: `
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1) inset;
`
}

View File

@ -0,0 +1,63 @@
import React from 'react'
import styled, { css } from 'styled-components'
type Props = {
disabled?: boolean
color?: 'primary' | 'secondary'
children: React.ReactNode
fullWidth?: boolean
onClick?: () => void
}
const Button = styled.button<Props>`
padding: 15px 25px;
${(props) => props.theme.mainShadow}
border: none;
border-radius: 12px;
color: ${(props) => props.theme.bg};
${(props) => props.theme.h3}
cursor: pointer;
transform: 300ms;
${(props) =>
props.fullWidth &&
css`
width: 100%;
`}
${(props) =>
props.color === 'secondary'
? css`
background: ${(props) => props.theme.secondary};
`
: css`
background: ${(props) => props.theme.primary};
`}
&:disabled {
opacity: 0.7;
cursor: default;
&:active {
transform: scale(1);
}
}
&:active {
transform: scale(0.95);
}
`
export default function MainButton({
disabled,
children,
color = 'primary',
fullWidth = false,
onClick
}: Props) {
return (
<Button onClick={onClick} disabled={disabled} color={color} fullWidth={fullWidth}>
{children}
</Button>
)
}

View File

@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react'
import styled, { css } from 'styled-components'
type Props = {
active: boolean
children: React.ReactNode
setActive: (param: boolean) => void
positionTop?: boolean
}
interface I_Container {
active: boolean
positionTop?: boolean
}
const Container = styled.div<I_Container>`
position: relative;
.target {
cursor: pointer;
}
.content {
z-index: 1;
position: absolute;
top: 0;
right: 120%;
transform: translateY(-50%);
${props => props.positionTop && css`transform: translateY(0%);`}
padding: 15px;
background: ${(props) => props.theme.bg};
${(props) => props.theme.mainShadow}
border-radius: 12px;
width: 200px;
flex-direction: column;
gap: 20px;
${(props) =>
props.active
? css`
display: flex;
`
: css`
display: none;
`}
}
`
export default function ContextMenu({ active, children, setActive, positionTop }: Props) {
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setActive(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
return (
<Container active={active} ref={menuRef} positionTop={positionTop}>
{children}
</Container>
)
}

View File

@ -0,0 +1,35 @@
import styled, { css } from 'styled-components'
type Props = {
value: 'off' | 'ok' | 'minus'
onClick: () => void
}
type ContainerProps = {
value: string
}
const Container = styled.div<ContainerProps>`
cursor: pointer;
width: 20px;
height: 20px;
border-radius: 6px;
border: 1px solid ${(props) => props.theme.textColor};
display: flex;
align-items: center;
justify-content: center;
${(props) =>
(props.value === 'ok' || props.value === 'minus') &&
css`
background: ${(props) => props.theme.textColor};
`}
`
export default function Checkbox({ value, onClick }: Props) {
return (
<Container onClick={onClick} value={value}>
{value === 'ok' && <img src='/src/images/ok.svg' />}
{value === 'minus' && <img src='/src/images/minus.svg' />}
</Container>
)
}

View File

@ -0,0 +1,62 @@
import React, { useRef } from 'react'
import styled from 'styled-components'
const InputContainer = styled.div`
position: relative;
width: 100%;
`
const Input = styled.input`
padding: 15px 20px;
border-radius: 12px;
background: ${(props) => props.theme.bg};
border: 1px solid ${props => props.theme.text};
font-size: 18px;
font-weight: 500;
outline: none;
width: 100%;
`
interface PlaceholderProps {
showPlaceholder: boolean
}
const Placeholder = styled.label<PlaceholderProps>`
background: ${(props) => props.theme.bg};
font-size: 18px;
font-weight: 500;
cursor: text;
position: absolute;
top: ${(props) => (props.showPlaceholder ? '30%' : '-20%')};
left: ${(props) => (props.showPlaceholder ? '22px' : '0px')};
border-radius: 5px;
transform: translateY(-50%);
transform: scale(${(props) => (props.showPlaceholder ? '1' : '0.85')});
color: #757575;
transition: 200ms;
padding: ${(props) => (props.showPlaceholder ? '0' : '0 10px')};
`
type Props = {
value: string | number
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
placeholder: string
type?: string
}
export default function MainInput({value, onChange, placeholder, type}: Props) {
const inputRef = useRef<HTMLInputElement>(null)
const activateInput = () => {
if (inputRef.current) {
inputRef.current.focus()
}
}
return (
<InputContainer onClick={activateInput}>
<Input type={type || 'text'} value={value} onChange={onChange} ref={inputRef} />
<Placeholder showPlaceholder={!value}>{placeholder}</Placeholder>
</InputContainer>
)
}

View File

@ -0,0 +1,42 @@
import React from 'react'
import styled from 'styled-components'
type Props = {
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
placeholder: string
}
const InputContainer = styled.div`
width: 100%;
display: flex;
input {
width: 100%;
${props => props.theme.innerShadow}
padding: 15px 25px;
border-radius: 12px;
outline: none;
border: none;
background: ${props => props.theme.bg};
${props => props.theme.paragraph}
}
img {
margin-left: -45px;
}
`
export default function SearchInput({ value, onChange, placeholder }: Props) {
return (
<InputContainer>
<input
type='text'
value={value}
onChange={onChange}
placeholder={placeholder}
/>
<img src='/src/images/search.svg' alt='' />
</InputContainer>
)
}

View File

@ -0,0 +1,64 @@
import React, { useRef } from 'react'
import styled from 'styled-components'
const TextareaContainer = styled.div`
position: relative;
textarea {
padding: 15px 20px;
border-radius: 12px;
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.text};
font-size: 18px;
font-weight: 500;
outline: none;
resize: none;
height: 110px;
width: 100%;
overflow: -moz-scrollbars-none;
scrollbar-width: none;
}
`
interface PlaceholderProps {
showPlaceholder: boolean
}
const Placeholder = styled.label<PlaceholderProps>`
background: ${(props) => props.theme.bg};
font-size: 18px;
font-weight: 500;
cursor: text;
position: absolute;
top: ${(props) => (props.showPlaceholder ? '15%' : '-8%')};
left: ${(props) => (props.showPlaceholder ? '22px' : '0px')};
border-radius: 5px;
transform: translateY(-50%);
transform: scale(${(props) => (props.showPlaceholder ? '1' : '0.85')});
color: #757575;
transition: 200ms;
padding: ${(props) => (props.showPlaceholder ? '0' : '0 10px')};
`
type Props = {
value: string | number
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
placeholder: string
}
export default function Textarea({ value, onChange, placeholder }: Props) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
const activateTextArea = () => {
if (textareaRef.current) {
textareaRef.current.focus()
}
}
return (
<TextareaContainer onClick={activateTextArea}>
<textarea value={value} onChange={onChange} ref={textareaRef}></textarea>
<Placeholder showPlaceholder={!value}>{placeholder}</Placeholder>
</TextareaContainer>
)
}

View File

@ -0,0 +1,39 @@
type Props = {}
import styled, { keyframes } from 'styled-components'
const loadingSpinnerSize = '33px'
const spinAnimation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
export const LoadingSpinner = styled.div`
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: ${loadingSpinnerSize};
height: ${loadingSpinnerSize};
img {
&:first-child {
animation: ${spinAnimation} 1s linear infinite;
}
position: absolute;
}
`
export default function Loader({}: Props) {
return (
<LoadingSpinner>
<img src="/src/images/loader/Spiner_1.svg" alt="" />
<img src="/src/images/loader/Spiner_2.svg" alt="" />
</LoadingSpinner>
)
}

View File

@ -0,0 +1,56 @@
import React from 'react'
import styled, { css } from 'styled-components'
type Props = {
children: React.ReactNode
modal: boolean
setModal: (param: boolean) => void
}
interface modalContainer {
modal: boolean
}
const Container = styled.div<modalContainer>`
${(props) =>
props.modal
? css`
display: block;
`
: css`
display: none;
`}
.bg {
z-index: 1;
position: fixed;
bottom: 0px;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
opacity: 0.7;
}
.content {
z-index: 2;
position: fixed;
padding: 20px;
border-radius: 12px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${(props) => props.theme.bg};
}
`
export default function Modal({ children, modal, setModal }: Props) {
return (
<Container modal={modal}>
<div className='bg' onClick={() => setModal(false)}></div>
<div className='content'>{children}</div>
</Container>
)
}

View File

@ -0,0 +1,75 @@
import React from 'react'
import styled, { css } from 'styled-components'
import Loader from '../loader/Loader'
type Props = {
loaderState?: boolean
children: React.ReactNode
}
interface loading {
loading?: boolean
}
const TableContainer = styled.table<loading>`
padding: 10px;
${(props) => props.theme.mainShadow}
border-radius: 12px;
width: 100%;
position: relative;
.loader {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: ${(props) => props.theme.bg};
z-index: 1;
opacity: 0.7;
${props => !props.loading && css`display: none;`}
}
.loader-container {
z-index: 2;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 30px;
background: ${(props) => props.theme.bg};
${(props) => props.theme.mainShadow}
border-radius: 12px;
${props => !props.loading && css`display: none;`}
}
table {
background: ${(props) => props.theme.bg};
border-collapse: collapse;
width: 100%;
}
td,
th {
border-bottom: 1px solid ${(props) => props.theme.text};
padding: 10px;
text-align: left;
}
tr:last-child td {
border-bottom: none;
}
`
export default function Table({ children, loaderState }: Props) {
return (
<TableContainer loading={loaderState}>
<div className='loader'></div>
<div className='loader-container'>
<Loader />
</div>
<table>{children}</table>
</TableContainer>
)
}

View File

@ -0,0 +1,304 @@
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { I_QueryParams } from 'src/services/type'
import styled, { css } from 'styled-components'
type Props = {
queryParams: I_QueryParams
setQueryParams: (param: I_QueryParams) => void
totalCount: number
}
interface I_Active {
active: boolean
}
const Container = styled.div`
display: flex;
gap: 50px;
align-items: start;
.page {
display: flex;
gap: 8px;
}
.rowsPerPage {
display: flex;
align-items: center;
gap: 10px;
}
.switcher-container {
position: relative;
}
.switcher {
cursor: pointer;
display: flex;
align-items: center;
}
`
const Menu = styled.div<I_Active>`
flex-direction: column;
border-radius: 12px;
padding: 5px;
${(props) => props.theme.mainShadow}
position: absolute;
z-index: 5;
bottom: -20%;
left: -30%;
transform: translate(-50% -50%);
background: ${(props) => props.theme.bg};
cursor: pointer;
${(props) =>
props.active
? css`
display: flex;
`
: css`
display: none;
`}
p {
padding: 5px 10px;
border-radius: 6px;
transition: 200ms;
&:hover {
background: ${(props) => props.theme.focusColor};
}
}
`
const Button = styled.div<I_Active>`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
width: 32px;
height: 32px;
${(props) =>
props.active
? css`
border: 1px solid #919eab;
`
: css`
cursor: default;
background: #919eab;
`}
&:last-of-type {
transform: rotate(180deg);
}
`
const Item = styled.div<I_Active>`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
width: 32px;
height: 32px;
${(props) =>
props.active
? css`
border: 1px solid ${(props) => props.theme.primary};
color: ${(props) => props.theme.primary};
`
: css`
border: 1px solid #919eab;
`}
`
export default function Pagination({
queryParams,
setQueryParams,
totalCount
}: Props) {
const [menuState, setMenuState] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const navigate = useNavigate()
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setMenuState(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const changeRowsPerPage = (value: number) => {
setQueryParams({ ...queryParams, count: value, page: 1 })
setMenuState(false)
const searchParams = new URLSearchParams(window.location.search)
if (searchParams.has('count')) {
searchParams.delete('count')
}
searchParams.append('count', value.toString())
if (searchParams.has('page')) {
searchParams.delete('page')
}
searchParams.append('page', '1')
navigate(`?${searchParams.toString()}`)
}
const handleChangePage = (pageNum: number) => {
setQueryParams({ ...queryParams, page: pageNum })
const searchParams = new URLSearchParams(window.location.search)
if (searchParams.has('page')) {
searchParams.delete('page')
}
searchParams.append('page', pageNum.toString())
navigate(`?${searchParams.toString()}`)
}
return (
<Container>
<div className='page'>
<Button
active={queryParams.page !== 1}
onClick={() => {
queryParams.page !== 1 && handleChangePage(queryParams.page - 1)
}}
>
<img src='/src/images/arrow.svg' alt='' />
</Button>
{Math.ceil(totalCount / queryParams.count) < 8 ? (
[...Array(Math.ceil(totalCount / queryParams.count)).keys()].map(
(item) => (
<Item
key={item}
active={item + 1 === queryParams.page}
onClick={() => handleChangePage(item + 1)}
>
{item + 1}
</Item>
)
)
) : (
<>
{queryParams.page < 5 ? (
<>
{[...Array(5).keys()].map((item) => (
<Item
active={item + 1 === queryParams.page}
onClick={() => handleChangePage(item + 1)}
>
{item + 1}
</Item>
))}
<Item active={false}>...</Item>
<Item
active={totalCount / queryParams.count === queryParams.page}
onClick={() =>
handleChangePage(totalCount / queryParams.count)
}
>
{totalCount / queryParams.count}
</Item>
</>
) : queryParams.page > 4 &&
queryParams.page <= totalCount / queryParams.count - 4 ? (
<>
<Item active={false} onClick={() => handleChangePage(1)}>
1
</Item>
<Item active={false}>...</Item>
<Item
active={false}
onClick={() => handleChangePage(queryParams.page - 1)}
>
{queryParams.page - 1}
</Item>
<Item
active={true}
onClick={() => handleChangePage(queryParams.page)}
>
{queryParams.page}
</Item>
<Item
active={false}
onClick={() => handleChangePage(queryParams.page + 1)}
>
{queryParams.page + 1}
</Item>
<Item active={false}>...</Item>
<Item
active={false}
onClick={() =>
handleChangePage(totalCount / queryParams.count)
}
>
10
</Item>
</>
) : (
<>
<Item active={false} onClick={() => handleChangePage(1)}>
1
</Item>
<Item active={false}>...</Item>
{[...Array(totalCount / queryParams.count).keys()].map(
(item) => (
<>
{item + 1 > totalCount / queryParams.count - 5 ? (
<Item
active={item + 1 === queryParams.page}
onClick={() => handleChangePage(item + 1)}
>
{item + 1}
</Item>
) : (
<></>
)}
</>
)
)}
</>
)}
</>
)}
<Button
active={queryParams.page !== Math.ceil(totalCount / queryParams.count)}
onClick={() => {
queryParams.page !== totalCount / queryParams.count &&
handleChangePage(queryParams.page + 1)
}}
>
<img src='/src/images/arrow.svg' alt='' />
</Button>
</div>
<div className='rowsPerPage'>
<p>Количество на странице:</p>
<div className='switcher-container'>
<div className='switcher' onClick={() => setMenuState(true)}>
<h3>{queryParams.count}</h3>
<img src='/src/images/bottom-arrow.svg' alt='' />
</div>
<Menu active={menuState} ref={menuRef}>
<p onClick={() => changeRowsPerPage(5)}>5</p>
<p onClick={() => changeRowsPerPage(10)}>10</p>
<p onClick={() => changeRowsPerPage(15)}>15</p>
<p onClick={() => changeRowsPerPage(20)}>20</p>
</Menu>
</div>
</div>
</Container>
)
}

View File

@ -0,0 +1,18 @@
import styled from 'styled-components'
type Props = {}
const Container = styled.div`
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10px;
`
export default function Footer({}: Props) {
return (
<Container>
<p> © ООО «Технологии и Коммуникации», 2022-2024</p>
</Container>
)
}

View File

@ -0,0 +1,54 @@
import React, { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SideBar from './SideBar'
import Footer from './Footer'
import axios from 'axios'
import { HOST_NAME } from 'src/constants/host'
type Props = {
children: React.ReactNode
}
const Container = styled.div`
padding-left: 260px;
padding-bottom: 40px;
`
export default function Layout({ children }: Props) {
const location = useLocation()
const currentPath = location.pathname
const navigate = useNavigate()
// авторизация
// useEffect(() => {
// const hasCookie = document.cookie.includes('_identid')
// if (!hasCookie) {
// navigate('/login')
// return
// }
// }, [])
useEffect(() => {
if (currentPath === '/') {
navigate('/registrations')
return
}
}, [location])
return (
<>
{currentPath !== '/login' ? (
<>
<SideBar />
<Container>{children}</Container>
</>
) : (
<>
<>{children}</>
</>
)}
<Footer />
</>
)
}

View File

@ -0,0 +1,78 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components'
const Container = styled.div`
position: fixed;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding-top: 40px;
background: ${(props) => props.theme.bg};
${(props) => props.theme.mainShadow}
width: 260px;
height: 100vh;
`
const SideBarItem = styled.a`
display: flex;
align-items: center;
gap: 20px;
border-radius: 12px;
width: 220px;
transition: 300ms;
padding: 10px 20px;
cursor: pointer;
&:hover {
background: ${(props) => props.theme.focusColor};
}
img {
width: 30px;
height: 30px;
}
`
type Props = {}
export default function SideBar({}: Props) {
return (
<Container>
<img src='src/images/logo.svg' alt='' />
<Link to={'/registrations'}>
<SideBarItem>
<img src='src/images/user.svg' alt='' />
<p>Регистрации</p>
</SideBarItem>
</Link>
<Link to={'/repository'}>
<SideBarItem>
<img src='src/images/box.svg' alt='' />
<p>Репозиторий</p>
</SideBarItem>
</Link>
<SideBarItem>
<img src='src/images/book.svg' alt='' />
<p>Логи</p>
</SideBarItem>
<Link to={'/store'}>
<SideBarItem>
<img src='src/images/openbox.svg' alt='' />
<p>Магазин</p>
</SideBarItem>
</Link>
<SideBarItem>
<img src='src/images/settings.svg' alt='' />
<p>Настройки</p>
</SideBarItem>
<SideBarItem>
<img src='src/images/admin.svg' alt='' />
<p>ГОСАдмин</p>
</SideBarItem>
<SideBarItem>
<img src='src/images/logout.svg' alt='' />
<p>Выход</p>
</SideBarItem>
</Container>
)
}

2
src/constants/host.ts Normal file
View File

@ -0,0 +1,2 @@
export const API_HOST_NAME = 'http://localhost:8070/api/v1'
export const HOST_NAME = 'http://localhost:8070'

3
src/images/Icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 12.5C8.62 12.5 7.5 11.38 7.5 10C7.5 8.62 8.62 7.5 10 7.5C11.38 7.5 12.5 8.62 12.5 10C12.5 11.38 11.38 12.5 10 12.5ZM10 5C7.23875 5 5 7.23875 5 10C5 12.7613 7.23875 15 10 15C12.7613 15 15 12.7613 15 10C15 7.23875 12.7613 5 10 5ZM37.5 21.41L30 13.75L17.5737 26.3888L12.5 21.25L2.5 30.4212V5C2.5 3.62 3.62 2.5 5 2.5H35C36.38 2.5 37.5 3.62 37.5 5V21.41ZM37.5 35C37.5 36.38 36.38 37.5 35 37.5H28.54L19.33 28.1688L30 17.4988L37.5 24.9988V35ZM5 37.5C3.62 37.5 2.5 36.38 2.5 35V33.8262L12.4313 24.9312L25.0013 37.5H5ZM35 0H5C2.23875 0 0 2.23875 0 5V35C0 37.7612 2.23875 40 5 40H35C37.7612 40 40 37.7612 40 35V5C40 2.23875 37.7612 0 35 0Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 804 B

12
src/images/admin.svg Normal file
View File

@ -0,0 +1,12 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_282_98)">
<path d="M15.0594 17.9621C17.3355 17.9621 19.1807 16.1169 19.1807 13.8408C19.1807 11.5646 17.3355 9.71948 15.0594 9.71948C12.7833 9.71948 10.9381 11.5646 10.9381 13.8408C10.9381 16.1169 12.7833 17.9621 15.0594 17.9621Z" fill="#292929"/>
<path d="M8.65212 22.7948C7.98583 23.8359 7.63367 25.0396 7.63367 26.2755V30.2145H22.4852V26.2755C22.4852 25.0395 22.133 23.8359 21.4667 22.7948C20.2739 20.9311 18.2408 19.8185 16.0281 19.8185H14.0907C11.878 19.8185 9.84487 20.9311 8.65212 22.7948Z" fill="#292929"/>
<path d="M28.3663 16.2648V10.8968L25.1213 10.1242C24.975 9.6959 24.8009 9.2762 24.6006 8.86891L26.3493 6.02729L22.5535 2.23149L19.7118 3.98022C19.3046 3.77996 18.8849 3.60584 18.4566 3.45947L17.684 0.214478H12.316L11.5434 3.45947C11.1151 3.60584 10.6954 3.77996 10.2882 3.98022L7.44654 2.23149L3.65068 6.02729L5.39935 8.86891C5.19909 9.2762 5.02497 9.6959 4.8786 10.1242L1.63367 10.8967V16.2648L4.87866 17.0374C5.02503 17.4657 5.19915 17.8854 5.39941 18.2927L3.64248 21.1477L6.2744 23.6687C6.48844 23.0271 6.78185 22.411 7.15105 21.8341C8.17313 20.2371 9.67919 19.0732 11.4131 18.4803C10.0395 17.3985 9.156 15.7209 9.156 13.8407C9.156 10.5856 11.8043 7.93725 15.0595 7.93725C18.3146 7.93725 20.9629 10.5856 20.9629 13.8407C20.9629 15.7209 20.0794 17.3985 18.7059 18.4803C20.4397 19.0732 21.9457 20.2371 22.9678 21.8341C23.3535 22.4368 23.6558 23.0824 23.872 23.7549C23.8719 23.7548 23.8719 23.7546 23.8719 23.7546L26.3364 21.1134L24.6007 18.2928C24.801 17.8855 24.9751 17.4658 25.1215 17.0375L28.3663 16.2648Z" fill="#292929"/>
</g>
<defs>
<clipPath id="clip0_282_98">
<rect width="30" height="30" fill="white" transform="translate(0 0.214478)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="26" viewBox="0 0 25 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4999 6.75V19.25M12.4999 19.25L17.7083 14.0417M12.4999 19.25L7.29159 14.0417" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

3
src/images/arrow.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="8" height="13" viewBox="0 0 8 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.65991 1.91L3.07991 6.5L7.65991 11.09L6.24991 12.5L0.249912 6.5L6.24991 0.5L7.65991 1.91Z" fill="#C4CDD5"/>
</svg>

After

Width:  |  Height:  |  Size: 220 B

3
src/images/book.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 24.5232V8.52319C5 7.12307 5 6.42299 5.27249 5.88822C5.51216 5.41781 5.89461 5.03536 6.36503 4.79568C6.8998 4.52319 7.59987 4.52319 9 4.52319H21C22.4001 4.52319 23.1003 4.52319 23.635 4.79568C24.1054 5.03536 24.4879 5.41781 24.7275 5.88822C25 6.42299 25 7.12307 25 8.52319V22.0232H7.5C6.11929 22.0232 5 23.1424 5 24.5232ZM5 24.5232C5 25.9039 6.11929 27.0232 7.5 27.0232H25M11.25 9.52319H18.75M11.25 14.5232H18.75M23.75 22.0232V27.0232" stroke="#292929" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 9.33997L12 13.92L16.59 9.33997L18 10.75L12 16.75L6 10.75L7.41 9.33997Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

4
src/images/box.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="31" height="25" viewBox="0 0 31 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.6858 3H4.4202C3.6664 3 3 3.468 3 4.2218V8.078H28V4.2218C28 3.4678 27.4396 3 26.6858 3Z" fill="#292929"/>
<path d="M20.3829 9.078V10.1046C20.3829 10.6428 19.9977 11.2264 19.4593 11.2264H11.6469C11.1085 11.2264 10.6173 10.6428 10.6173 10.1046V9.078H3.78131V20.2608C3.78131 21.0146 4.44771 21.7732 5.20151 21.7732H25.9045C26.6585 21.7732 27.2187 21.0144 27.2187 20.2608V9.078H20.3829Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

3
src/images/close.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6668 16.6666L12.5002 12.4999M12.5002 12.4999L8.3335 8.33325M12.5002 12.4999L16.6668 8.33325M12.5002 12.4999L8.3335 16.6666" stroke="#292929" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

3
src/images/download.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4538 4.29694C11.25 4.69694 11.25 5.22194 11.25 6.27319V14.5232H9.75375C8.65 14.5232 8.0975 14.5232 7.83625 14.7432C7.72415 14.8374 7.63568 14.9566 7.57794 15.0912C7.5202 15.2258 7.49479 15.372 7.50375 15.5182C7.525 15.8607 7.90625 16.2594 8.6675 17.0582L13.915 22.5594C14.2925 22.9569 14.4813 23.1544 14.7025 23.2282C14.8956 23.2929 15.1044 23.2929 15.2975 23.2282C15.5187 23.1544 15.7075 22.9569 16.085 22.5594L21.3325 17.0594C22.095 16.2594 22.475 15.8594 22.495 15.5182C22.5041 15.3721 22.4789 15.2259 22.4214 15.0914C22.3639 14.9568 22.2756 14.8376 22.1638 14.7432C21.9025 14.5232 21.3513 14.5232 20.2463 14.5232H18.75V6.27319C18.75 5.22319 18.75 4.69819 18.545 4.29694C18.3655 3.94402 18.0789 3.65697 17.7262 3.47694C17.3262 3.27319 16.8013 3.27319 15.75 3.27319H14.25C13.2 3.27319 12.675 3.27319 12.2738 3.47694C11.9206 3.65676 11.6336 3.94383 11.4538 4.29694ZM6.25 27.0232C6.25 27.3547 6.3817 27.6727 6.61612 27.9071C6.85054 28.1415 7.16848 28.2732 7.5 28.2732H22.5C22.8315 28.2732 23.1495 28.1415 23.3839 27.9071C23.6183 27.6727 23.75 27.3547 23.75 27.0232C23.75 26.6917 23.6183 26.3737 23.3839 26.1393C23.1495 25.9049 22.8315 25.7732 22.5 25.7732H7.5C7.16848 25.7732 6.85054 25.9049 6.61612 26.1393C6.3817 26.3737 6.25 26.6917 6.25 27.0232Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="26" viewBox="0 0 18 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.45375 1.29694C5.25 1.69694 5.25 2.22194 5.25 3.27319V11.5232H3.75375C2.65 11.5232 2.0975 11.5232 1.83625 11.7432C1.72415 11.8374 1.63568 11.9566 1.57794 12.0912C1.5202 12.2258 1.49479 12.372 1.50375 12.5182C1.525 12.8607 1.90625 13.2594 2.6675 14.0582L7.915 19.5594C8.2925 19.9569 8.48125 20.1544 8.7025 20.2282C8.89556 20.2929 9.10444 20.2929 9.2975 20.2282C9.51875 20.1544 9.7075 19.9569 10.085 19.5594L15.3325 14.0594C16.095 13.2594 16.475 12.8594 16.495 12.5182C16.5041 12.3721 16.4789 12.2259 16.4214 12.0914C16.3639 11.9568 16.2756 11.8376 16.1638 11.7432C15.9025 11.5232 15.3513 11.5232 14.2463 11.5232H12.75V3.27319C12.75 2.22319 12.75 1.69819 12.545 1.29694C12.3655 0.944018 12.0789 0.656972 11.7262 0.476943C11.3262 0.273193 10.8013 0.273193 9.75 0.273193H8.25C7.2 0.273193 6.675 0.273193 6.27375 0.476943C5.92063 0.65676 5.63357 0.943828 5.45375 1.29694ZM0.25 24.0232C0.25 24.3547 0.381696 24.6727 0.616117 24.9071C0.850537 25.1415 1.16848 25.2732 1.5 25.2732H16.5C16.8315 25.2732 17.1495 25.1415 17.3839 24.9071C17.6183 24.6727 17.75 24.3547 17.75 24.0232C17.75 23.6917 17.6183 23.3737 17.3839 23.1393C17.1495 22.9049 16.8315 22.7732 16.5 22.7732H1.5C1.16848 22.7732 0.850537 22.9049 0.616117 23.1393C0.381696 23.3737 0.25 23.6917 0.25 24.0232Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

3
src/images/edit.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.32507 14.045L2.23007 16.965C2.18802 17.0774 2.1792 17.1995 2.20468 17.3167C2.23015 17.434 2.28883 17.5414 2.37372 17.6262C2.4586 17.711 2.5661 17.7696 2.68337 17.7949C2.80064 17.8203 2.92274 17.8113 3.03507 17.7692L5.95423 16.6742C6.28835 16.549 6.59182 16.3538 6.84423 16.1017L15.3001 7.64584C15.3001 7.64584 15.0051 6.76167 14.1217 5.87751C13.2384 4.99418 12.3534 4.69918 12.3534 4.69918L3.89757 13.155C3.64541 13.4074 3.45021 13.7109 3.32507 14.045ZM13.5326 3.52001L14.6851 2.36751C14.8917 2.16084 15.1676 2.02918 15.4559 2.07751C15.8617 2.14418 16.4826 2.34584 17.0676 2.93168C17.6534 3.51751 17.8551 4.13751 17.9217 4.54334C17.9701 4.83168 17.8384 5.10751 17.6317 5.31418L16.4784 6.46668C16.4784 6.46668 16.1842 5.58334 15.3001 4.70001C14.4167 3.81501 13.5326 3.52001 13.5326 3.52001Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.55556 30.0003C2.03959 30.0003 -0.0354614 32.0515 0.345123 34.5385C1.3056 40.815 4.2417 46.6684 8.7868 51.2135C13.3319 55.7586 19.1852 58.6947 25.4617 59.6552C27.9488 60.0357 30 57.9607 30 55.4447V55.2013C30 52.6854 27.9361 50.696 25.4812 50.1452C21.6935 49.2955 18.1897 47.3871 15.4014 44.5988C12.6131 41.8105 10.7048 38.3068 9.85503 34.5191C9.30429 32.0641 7.31491 30.0003 4.79895 30.0003H4.55556Z" fill="#006666"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@ -0,0 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M60 30C60 46.5685 46.5685 60 30 60C13.4315 60 0 46.5685 0 30C0 13.4315 13.4315 0 30 0C46.5685 0 60 13.4315 60 30ZM9.35451 30C9.35451 41.4022 18.5978 50.6455 30 50.6455C41.4022 50.6455 50.6455 41.4022 50.6455 30C50.6455 18.5978 41.4022 9.35451 30 9.35451C18.5978 9.35451 9.35451 18.5978 9.35451 30Z" fill="#006666"/>
</svg>

After

Width:  |  Height:  |  Size: 442 B

9
src/images/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 116 KiB

3
src/images/logout.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="31" height="30" viewBox="0 0 31 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 9.02354V20.4789C5 21.5335 5.43969 22.471 6.14281 23.2032C6.84594 23.9064 7.75438 24.287 8.80906 24.287H14.5512V21.9142H8.80906C8.01781 21.9142 7.40281 21.2692 7.40281 20.4789V9.02354C7.40281 8.23229 8.01781 7.61729 8.80906 7.61729H14.5512V5.21448H8.80906C7.75438 5.21448 6.84594 5.6251 6.14281 6.32823C5.43969 7.06042 5 7.96885 5 9.02354ZM11.2991 12.481V17.0514C11.2991 17.5792 11.7678 18.0179 12.2956 18.0179H17.5981V21.6217C17.5981 21.9442 17.7744 22.2076 18.0669 22.3539C18.1841 22.3829 18.3013 22.3829 18.3603 22.3829C18.5656 22.3829 18.7409 22.3239 18.8881 22.1776L25.7731 15.2926C26.0956 15.0292 26.0666 14.5014 25.7731 14.2089L18.8881 7.35292C18.4775 6.91323 17.5991 7.17667 17.5991 7.88073V11.5135H12.2966C11.7688 11.5135 11.3 11.9532 11.3 12.4801L11.2991 12.481Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 905 B

5
src/images/menu.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="25" height="26" viewBox="0 0 25 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.7917 14.0417C20.367 14.0417 20.8333 13.5754 20.8333 13C20.8333 12.4247 20.367 11.9584 19.7917 11.9584C19.2164 11.9584 18.75 12.4247 18.75 13C18.75 13.5754 19.2164 14.0417 19.7917 14.0417Z" stroke="#292929" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.4999 14.0417C13.0752 14.0417 13.5416 13.5754 13.5416 13C13.5416 12.4247 13.0752 11.9584 12.4999 11.9584C11.9246 11.9584 11.4583 12.4247 11.4583 13C11.4583 13.5754 11.9246 14.0417 12.4999 14.0417Z" stroke="#292929" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.20841 14.0417C5.78371 14.0417 6.25008 13.5754 6.25008 13C6.25008 12.4247 5.78371 11.9584 5.20841 11.9584C4.63312 11.9584 4.16675 12.4247 4.16675 13C4.16675 13.5754 4.63312 14.0417 5.20841 14.0417Z" stroke="#292929" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 974 B

3
src/images/minus.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="14" height="2" viewBox="0 0 14 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 1C14 1.26522 13.9078 1.51957 13.7437 1.70711C13.5796 1.89464 13.3571 2 13.125 2H0.875C0.642936 2 0.420376 1.89464 0.256282 1.70711C0.0921872 1.51957 0 1.26522 0 1C0 0.734784 0.0921872 0.48043 0.256282 0.292893C0.420376 0.105357 0.642936 0 0.875 0H13.125C13.3571 0 13.5796 0.105357 13.7437 0.292893C13.9078 0.48043 14 0.734784 14 1Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

9
src/images/off.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

3
src/images/ok.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.7099 1.20986C14.617 1.11613 14.5064 1.04174 14.3845 0.990969C14.2627 0.940201 14.132 0.914062 13.9999 0.914062C13.8679 0.914062 13.7372 0.940201 13.6154 0.990969C13.4935 1.04174 13.3829 1.11613 13.29 1.20986L5.83995 8.66986L2.70995 5.52986C2.61343 5.43662 2.49949 5.36331 2.37463 5.3141C2.24978 5.2649 2.11645 5.24077 1.98227 5.24309C1.84809 5.24541 1.71568 5.27414 1.5926 5.32763C1.46953 5.38113 1.35819 5.45834 1.26495 5.55486C1.17171 5.65138 1.0984 5.76532 1.04919 5.89018C0.999989 6.01503 0.975859 6.14836 0.97818 6.28254C0.980502 6.41672 1.00923 6.54913 1.06272 6.67221C1.11622 6.79529 1.19343 6.90662 1.28995 6.99986L5.12995 10.8399C5.22291 10.9336 5.33351 11.008 5.45537 11.0588C5.57723 11.1095 5.70794 11.1357 5.83995 11.1357C5.97196 11.1357 6.10267 11.1095 6.22453 11.0588C6.34639 11.008 6.45699 10.9336 6.54995 10.8399L14.7099 2.67986C14.8115 2.58622 14.8925 2.47257 14.9479 2.34607C15.0033 2.21957 15.0319 2.08296 15.0319 1.94486C15.0319 1.80676 15.0033 1.67015 14.9479 1.54365C14.8925 1.41715 14.8115 1.3035 14.7099 1.20986Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

9
src/images/on.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

4
src/images/openbox.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.7188 11.3707L27.9688 9.12074C28.1563 8.93324 28.25 8.55824 28.25 8.27699C28.1563 7.99574 27.9688 7.71449 27.6875 7.62074L18.3125 3.87074C17.9375 3.68324 17.5625 3.77699 17.2812 4.05824L15.125 6.21449L12.9688 4.05824C12.6875 3.77699 12.3125 3.68324 11.9375 3.87074L2.5625 7.62074C2.28125 7.71449 2.09375 7.99574 2 8.27699C2 8.55824 2.09375 8.93324 2.28125 9.12074L4.53125 11.3707L2.1875 14.4645C2 14.652 2 15.027 2 15.3082C2.09375 15.5895 2.28125 15.777 2.5625 15.9645L11.9375 19.7145C12.0313 19.7145 12.125 19.8082 12.3125 19.8082C12.5938 19.8082 12.875 19.7145 13.0625 19.4332L15.125 16.7145L17.1875 19.4332C17.375 19.7145 17.6563 19.8082 17.9375 19.8082C18.0313 19.8082 18.2188 19.8082 18.3125 19.7145L27.6875 15.9645C27.9688 15.8707 28.1563 15.5895 28.25 15.3082C28.3438 15.027 28.25 14.7457 28.0625 14.4645L25.7188 11.3707ZM15.125 13.9957L8.28125 11.277L15.125 8.55824L21.9688 11.277L15.125 13.9957Z" fill="#292929"/>
<path d="M17.9375 21.5895C17.0938 21.5895 16.25 21.2145 15.6875 20.4645L15.125 19.7145L14.5625 20.4645C13.8125 21.4957 12.4062 21.8707 11.2812 21.402L4.8125 18.777V22.527C4.8125 22.902 5 23.277 5.375 23.3707L14.75 27.1207C14.8438 27.1207 14.9375 27.2145 15.125 27.2145C15.3125 27.2145 15.4062 27.2145 15.5 27.1207L24.875 23.3707C25.25 23.1832 25.4375 22.902 25.4375 22.527V18.777L18.9688 21.402C18.6875 21.4957 18.3125 21.5895 17.9375 21.5895Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

3
src/images/params.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6662 0C14.3125 4.21276e-06 13.9732 0.140441 13.7231 0.390416C13.473 0.640391 13.3325 0.979428 13.3325 1.33294V2.00006H1.33324C0.979518 2.00007 0.640288 2.1405 0.390169 2.39046C0.140049 2.64042 -0.000474239 2.97944 -0.000488281 3.33294C-0.000489881 3.50799 0.0340067 3.68131 0.101032 3.84303C0.168057 4.00475 0.266298 4.1517 0.390146 4.27547C0.513993 4.39925 0.661022 4.49743 0.822838 4.56442C0.984654 4.6314 1.15809 4.66588 1.33324 4.66588H13.3325V5.33379C13.3325 5.6873 13.473 6.02634 13.7231 6.27632C13.9732 6.52629 14.3125 6.66673 14.6662 6.66673C15.0199 6.66673 15.3591 6.52629 15.6093 6.27632C15.8594 6.02634 15.9999 5.6873 15.9999 5.33379V4.66588H18.6657C18.8409 4.66589 19.0143 4.63142 19.1761 4.56444C19.338 4.49745 19.485 4.39927 19.6089 4.27549C19.7327 4.15172 19.831 4.00477 19.898 3.84305C19.965 3.68133 19.9995 3.50799 19.9995 3.33294C19.9995 3.1579 19.965 2.98457 19.898 2.82286C19.8309 2.66114 19.7327 2.5142 19.6088 2.39043C19.485 2.26666 19.3379 2.16849 19.1761 2.10151C19.0143 2.03453 18.8409 2.00006 18.6657 2.00006H15.9999V1.33294C15.9999 0.979428 15.8594 0.640391 15.6093 0.390416C15.3591 0.140441 15.0199 4.21276e-06 14.6662 0ZM4.99951 6.66667C4.82436 6.66666 4.65092 6.70113 4.48909 6.76812C4.32727 6.8351 4.18024 6.93328 4.05638 7.05706C3.93253 7.18083 3.83428 7.32778 3.76725 7.4895C3.70022 7.65123 3.66572 7.82456 3.66572 7.99961V8.66667H1.33363C1.15848 8.66667 0.985044 8.70115 0.823229 8.76813C0.661413 8.83512 0.514384 8.93331 0.390536 9.05708C0.266688 9.18086 0.168447 9.3278 0.101422 9.48952C0.0343973 9.65124 -9.92786e-05 9.82457 -9.76621e-05 9.99961C-9.92786e-05 10.1747 0.0343973 10.348 0.101422 10.5097C0.168447 10.6714 0.266688 10.8184 0.390536 10.9421C0.514384 11.0659 0.661413 11.1641 0.823229 11.2311C0.985044 11.2981 1.15848 11.3326 1.33363 11.3326H3.66572V12.0005C3.66572 12.1755 3.70022 12.3488 3.76725 12.5106C3.83428 12.6723 3.93253 12.8192 4.05638 12.943C4.18024 13.0668 4.32727 13.165 4.48909 13.2319C4.65092 13.2989 4.82436 13.3334 4.99951 13.3334C5.35324 13.3334 5.69248 13.193 5.9426 12.943C6.19272 12.693 6.33324 12.354 6.33324 12.0005V11.3326H18.6662C18.8413 11.3326 19.0148 11.2981 19.1766 11.2311C19.3384 11.1641 19.4854 11.0659 19.6093 10.9421C19.7331 10.8184 19.8314 10.6714 19.8984 10.5097C19.9654 10.348 19.9999 10.1747 19.9999 9.99961C19.9999 9.82457 19.9654 9.65124 19.8984 9.48952C19.8314 9.3278 19.7331 9.18086 19.6093 9.05708C19.4854 8.93331 19.3384 8.83512 19.1766 8.76813C19.0148 8.70115 18.8413 8.66667 18.6662 8.66667H6.33324V7.99961C6.33324 7.64609 6.19272 7.30706 5.9426 7.05708C5.69248 6.80711 5.35324 6.66667 4.99951 6.66667ZM9.33285 13.3334C8.97913 13.3334 8.6399 13.4738 8.38978 13.7238C8.13966 13.9738 7.99914 14.3128 7.99912 14.6663V15.3333H1.33363C1.15848 15.3333 0.985044 15.3678 0.823229 15.4348C0.661413 15.5018 0.514384 15.6 0.390536 15.7237C0.266688 15.8475 0.168447 15.9945 0.101422 16.1562C0.0343973 16.3179 -9.92786e-05 16.4912 -9.76621e-05 16.6663C-8.35946e-05 17.0198 0.140439 17.3588 0.390559 17.6088C0.640679 17.8587 0.979909 17.9992 1.33363 17.9992H7.99912V18.6671C7.99914 19.0206 8.13966 19.3596 8.38978 19.6096C8.6399 19.8596 8.97913 20 9.33285 20C9.68656 20 10.0258 19.8596 10.2759 19.6096C10.526 19.3596 10.6666 19.0206 10.6666 18.6671V17.9992H18.6662C19.0199 17.9992 19.3591 17.8587 19.6092 17.6088C19.8594 17.3588 19.9999 17.0198 19.9999 16.6663C19.9999 16.4912 19.9654 16.3179 19.8984 16.1562C19.8314 15.9945 19.7331 15.8475 19.6093 15.7237C19.4854 15.6 19.3384 15.5018 19.1766 15.4348C19.0148 15.3678 18.8413 15.3333 18.6662 15.3333H10.6666V14.6663C10.6666 14.3128 10.526 13.9738 10.2759 13.7238C10.0258 13.4738 9.68656 13.3334 9.33285 13.3334Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

10
src/images/play.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_61_2312)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.415 6.61716L5.14475 0.365623C3.42531 -0.680357 1.28564 0.642997 1.28564 2.74871V15.2512C1.28564 17.3596 3.42531 18.6802 5.14475 17.6342L15.415 11.3859C17.1473 10.3316 17.1473 7.67137 15.415 6.61716Z" fill="#292929"/>
</g>
<defs>
<clipPath id="clip0_61_2312">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 515 B

3
src/images/reload.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75567 5.18403C9.87218 5.30058 9.95152 5.44905 9.98366 5.61067C10.0158 5.7723 9.99929 5.93983 9.93624 6.09208C9.87318 6.24433 9.76639 6.37446 9.62939 6.46603C9.49238 6.5576 9.3313 6.6065 9.1665 6.60653H3.33317V14.9399H7.49984C7.72085 14.9399 7.93281 15.0277 8.08909 15.1839C8.24537 15.3402 8.33317 15.5522 8.33317 15.7732C8.33317 15.9942 8.24537 16.2062 8.08909 16.3625C7.93281 16.5187 7.72085 16.6065 7.49984 16.6065H2.49984C2.27882 16.6065 2.06686 16.5187 1.91058 16.3625C1.7543 16.2062 1.6665 15.9942 1.6665 15.7732V5.7732C1.6665 5.55219 1.7543 5.34022 1.91058 5.18394C2.06686 5.02766 2.27882 4.93987 2.49984 4.93987H7.15484L6.07734 3.86237C5.99775 3.78549 5.93426 3.69354 5.89059 3.59187C5.84691 3.4902 5.82392 3.38085 5.82296 3.2702C5.822 3.15955 5.84308 3.04982 5.88499 2.9474C5.92689 2.84499 5.98876 2.75195 6.06701 2.6737C6.14525 2.59546 6.2383 2.53358 6.34071 2.49168C6.44312 2.44978 6.55286 2.4287 6.66351 2.42966C6.77415 2.43062 6.8835 2.45361 6.98517 2.49728C7.08684 2.54096 7.1788 2.60444 7.25567 2.68403L9.75567 5.18403ZM17.4998 4.93987H12.4998C12.2788 4.93987 12.0669 5.02766 11.9106 5.18394C11.7543 5.34022 11.6665 5.55219 11.6665 5.7732C11.6665 5.99421 11.7543 6.20617 11.9106 6.36246C12.0669 6.51874 12.2788 6.60653 12.4998 6.60653H16.6665V14.9399H10.8332C10.6684 14.9399 10.5073 14.9888 10.3703 15.0804C10.2333 15.1719 10.1265 15.3021 10.0634 15.4543C10.0004 15.6066 9.98388 15.7741 10.016 15.9357C10.0482 16.0974 10.1275 16.2458 10.244 16.3624L12.744 18.8624C12.9012 19.0142 13.1117 19.0982 13.3302 19.0963C13.5487 19.0944 13.7577 19.0067 13.9122 18.8522C14.0667 18.6977 14.1543 18.4887 14.1562 18.2702C14.1581 18.0517 14.0741 17.8412 13.9223 17.684L12.8448 16.6065H17.4998C17.7208 16.6065 17.9328 16.5187 18.0891 16.3625C18.2454 16.2062 18.3332 15.9942 18.3332 15.7732V5.7732C18.3332 5.55219 18.2454 5.34022 18.0891 5.18394C17.9328 5.02766 17.7208 4.93987 17.4998 4.93987Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

3
src/images/search.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.3672 17.3346L21.875 21.875M19.7917 11.4583C19.7917 16.0607 16.0607 19.7917 11.4583 19.7917C6.85596 19.7917 3.125 16.0607 3.125 11.4583C3.125 6.85596 6.85596 3.125 11.4583 3.125C16.0607 3.125 19.7917 6.85596 19.7917 11.4583Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 422 B

3
src/images/settings.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.8485 2.90478C17.3856 2.71448 16.7987 2.71448 15.625 2.71448C14.4512 2.71448 13.8644 2.71448 13.4015 2.90478C12.7842 3.15852 12.2939 3.6452 12.0382 4.25777C11.9215 4.5374 11.8758 4.8626 11.8579 5.33696C11.8317 6.03408 11.4714 6.67934 10.8627 7.02814C10.254 7.37693 9.5108 7.3639 8.88936 7.03793C8.46647 6.8161 8.15986 6.69275 7.85749 6.65325C7.19511 6.5667 6.52522 6.74484 5.9952 7.14848C5.59767 7.4512 5.30423 7.9556 4.71737 8.96439C4.13051 9.97319 3.83708 10.4776 3.77168 10.9706C3.68447 11.628 3.86397 12.2928 4.27068 12.8189C4.45632 13.059 4.71721 13.2607 5.12212 13.5132C5.71738 13.8845 6.1004 14.5169 6.10036 15.2145C6.10032 15.9121 5.71732 16.5444 5.12212 16.9155C4.71715 17.1681 4.45621 17.37 4.27056 17.6101C3.86385 18.1361 3.68436 18.8009 3.77156 19.4582C3.83696 19.9512 4.1304 20.4557 4.71725 21.4645C5.30411 22.4732 5.59755 22.9777 5.99507 23.2803C6.5251 23.684 7.19498 23.8621 7.85736 23.7756C8.15972 23.7361 8.46631 23.6127 8.88916 23.391C9.51065 23.065 10.2539 23.052 10.8626 23.4007C11.4714 23.7496 11.8317 24.3948 11.8579 25.0921C11.8758 25.5663 11.9215 25.8916 12.0382 26.1712C12.2939 26.7837 12.7842 27.2705 13.4015 27.5242C13.8644 27.7145 14.4512 27.7145 15.625 27.7145C16.7987 27.7145 17.3856 27.7145 17.8485 27.5242C18.4657 27.2705 18.9561 26.7837 19.2117 26.1712C19.3285 25.8916 19.3742 25.5664 19.3921 25.092C19.4184 24.3949 19.7785 23.7496 20.3872 23.4007C20.996 23.0519 21.7392 23.065 22.3607 23.391C22.7836 23.6127 23.0901 23.736 23.3925 23.7755C24.0549 23.8621 24.7247 23.684 25.2547 23.2803C25.6524 22.9776 25.9457 22.4732 26.5326 21.4644C27.1195 20.4556 27.4129 19.9512 27.4784 19.4582C27.5655 18.8009 27.386 18.136 26.9794 17.61C26.7936 17.3699 26.5327 17.168 26.1277 16.9155C25.5326 16.5444 25.1496 15.912 25.1496 15.2144C25.1496 14.5167 25.5326 13.8846 26.1277 13.5135C26.5329 13.2609 26.7937 13.0591 26.9795 12.8189C27.3861 12.2929 27.5656 11.6281 27.4785 10.9707C27.413 10.4777 27.1196 9.97326 26.5327 8.96448C25.9459 7.95569 25.6525 7.45129 25.2549 7.14856C24.7249 6.74493 24.055 6.56679 23.3926 6.65334C23.0902 6.69284 22.7836 6.81618 22.3609 7.03798C21.7394 7.36396 20.9961 7.377 20.3874 7.02818C19.7786 6.67936 19.4184 6.03405 19.392 5.3369C19.3741 4.86258 19.3285 4.53739 19.2117 4.25777C18.9561 3.6452 18.4657 3.15852 17.8485 2.90478ZM15.625 18.9645C17.7119 18.9645 19.4035 17.2856 19.4035 15.2145C19.4035 13.1434 17.7119 11.4645 15.625 11.4645C13.5381 11.4645 11.8464 13.1434 11.8464 15.2145C11.8464 17.2856 13.5381 18.9645 15.625 18.9645Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

13
src/images/stop.svg Normal file
View File

@ -0,0 +1,13 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_61_2486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.415 6.61716L5.14475 0.365623C3.42531 -0.680357 1.28564 0.642997 1.28564 2.74871V15.2512C1.28564 17.3596 3.42531 18.6802 5.14475 17.6342L15.415 11.3859C17.1473 10.3316 17.1473 7.67137 15.415 6.61716Z" fill="#292929"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.28711 6.61716L13.4623 0.365623C15.1658 -0.680357 17.2856 0.642997 17.2856 2.74871V15.2512C17.2856 17.3596 15.1658 18.6802 13.4623 17.6342L3.28711 11.3859C1.57082 10.3316 1.57082 7.67137 3.28711 6.61716Z" fill="#292929"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.44895 11.5272L2.30107 3.14838C1.43976 1.74561 2.52948 2.31443e-08 4.26345 9.89385e-08L14.5587 5.48957e-07C16.2949 6.24849e-07 17.3824 1.74561 16.521 3.14838L11.3758 11.5272C10.5077 12.9405 8.31704 12.9405 7.44895 11.5272Z" fill="#292929"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51423 5.17895L2.30468 14.4982C1.43304 16.0585 2.53582 18 4.29057 18L14.7091 18C16.4661 18 17.5667 16.0585 16.695 14.4982L11.4881 5.17895C10.6096 3.60702 8.39272 3.60702 7.51423 5.17895Z" fill="#292929"/>
</g>
<defs>
<clipPath id="clip0_61_2486">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

11
src/images/trash.svg Normal file
View File

@ -0,0 +1,11 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_88_910)">
<path d="M6.66675 1.74996V2.58329H2.50008C2.03985 2.58329 1.66675 2.95639 1.66675 3.41663V4.24996C1.66675 4.71019 2.03985 5.08329 2.50008 5.08329H17.5001C17.9603 5.08329 18.3334 4.71019 18.3334 4.24996V3.41663C18.3334 2.95639 17.9603 2.58329 17.5001 2.58329H13.3334V1.74996C13.3334 1.28972 12.9603 0.916626 12.5001 0.916626H7.50008C7.03985 0.916626 6.66675 1.28972 6.66675 1.74996Z" fill="#292929"/>
<path d="M3.26929 6.75H16.7306L15.9454 17.3513C15.8486 18.6568 14.7613 19.6667 13.4522 19.6667H6.54774C5.23867 19.6667 4.15127 18.6568 4.05457 17.3513L3.26929 6.75Z" fill="#292929"/>
</g>
<defs>
<clipPath id="clip0_88_910">
<rect width="20" height="20" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 855 B

4
src/images/user.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.225 25.0209H24.775C24.9005 25.0227 25.0249 24.9976 25.1399 24.9473C25.2549 24.8969 25.3578 24.8226 25.4417 24.7292C25.5095 24.6534 25.5606 24.5641 25.5915 24.4671C25.6224 24.3702 25.6324 24.2678 25.6208 24.1667C25.4006 22.454 24.7711 20.8195 23.7854 19.4015C22.7998 17.9836 21.487 16.824 19.9583 16.0209C19.308 16.6735 18.5353 17.1913 17.6844 17.5446C16.8335 17.8979 15.9213 18.0797 15 18.0797C14.0787 18.0797 13.1665 17.8979 12.3156 17.5446C11.4647 17.1913 10.692 16.6735 10.0417 16.0209C8.51295 16.824 7.20022 17.9836 6.21457 19.4015C5.22892 20.8195 4.59935 22.454 4.37917 24.1667C4.36635 24.2698 4.37571 24.3744 4.40661 24.4736C4.43751 24.5727 4.48924 24.6642 4.55833 24.7417C4.64332 24.8328 4.74672 24.9048 4.86165 24.953C4.97659 25.0011 5.10044 25.0242 5.225 25.0209Z" fill="#292929"/>
<path d="M10.5959 15.25C10.7084 15.3708 10.8292 15.4833 10.95 15.5916C12.061 16.5919 13.503 17.1453 14.9979 17.1453C16.4929 17.1453 17.9348 16.5919 19.0459 15.5916C19.1713 15.4842 19.2909 15.3702 19.4042 15.25C19.5167 15.1291 19.6292 15.0083 19.7334 14.8791C20.4546 13.9858 20.9083 12.9065 21.042 11.7661C21.1757 10.6257 20.9839 9.47077 20.4888 8.43479C19.9937 7.39881 19.2156 6.5241 18.2443 5.91177C17.273 5.29944 16.1482 4.97449 15 4.97449C13.8518 4.97449 12.7271 5.29944 11.7558 5.91177C10.7845 6.5241 10.0063 7.39881 9.51125 8.43479C9.01617 9.47077 8.82438 10.6257 8.95806 11.7661C9.09175 12.9065 9.54544 13.9858 10.2667 14.8791C10.3709 15 10.4834 15.1291 10.5959 15.25Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,7 +1,19 @@
import React from 'react' import '@fontsource/roboto'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx' import { ThemeProvider } from 'styled-components'
import App from './App'
import { GlobalStyles } from './assets/globalStyles'
import { theme } from './assets/theme'
import Layout from './components/layout/Layout'
import { BrowserRouter } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<GlobalStyles />
<Layout>
<App /> <App />
</Layout>
</ThemeProvider>
</BrowserRouter>
) )

81
src/pages/Login.tsx Normal file
View File

@ -0,0 +1,81 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Loader from 'src/components/UI/loader/Loader'
import UserService from 'src/services/userSrvices'
import styled from 'styled-components'
type Props = {}
type T_User = {
name: string
password: string
}
const Container = styled.div`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 400px;
display: flex;
flex-direction: column;
gap: 20px;
text-align: center;
`
export default function Login({}: Props) {
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const [user, setUser] = useState<T_User>({ name: '', password: '' })
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, name: e.target.value })
}
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, password: e.target.value })
}
const loginUser = async () => {
setLoading(true)
try {
const response = await UserService.login(user.name, user.password)
if (response.status === 200) {
navigate('/')
} else {
setError(true)
}
} catch (error) {
console.log(error)
}
setLoading(false)
}
return (
<>
<Container>
<h1>Личный кабинет</h1>
<MainInput
value={user.name}
onChange={handleNameChange}
placeholder='Имя пользователя'
/>
<MainInput
type='password'
value={user.password}
onChange={handlePasswordChange}
placeholder='Пароль'
/>
<MainButton fullWidth={true} onClick={loginUser}>
Войти
</MainButton>
{error && <p>Проблемма авторизации</p>}
</Container>
{loading && <Loader />}
</>
)
}

View File

@ -0,0 +1,233 @@
import { useEffect, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import SearchInput from 'src/components/UI/input/SearchInput'
import Loader from 'src/components/UI/loader/Loader'
import Pagination from 'src/components/UI/table/pagination/Pagination'
import RegistrationsService from 'src/services/registrationsServices'
import { I_QueryParams } from 'src/services/type'
import NewRegModal from './blocks/NewRegModal'
import RegTable from './blocks/RegTable'
import {
RegContainer,
RegLoaderContainer,
RegNotification,
RegSelectNotification,
SearchNotification
} from './styles'
import { T_ColumnsState, T_NewRegistration, T_RegistrationItem } from './types'
type Props = {}
export default function Registrations({}: Props) {
const [errorState, setErrorState] = useState(false)
const [loadingState, setLoadingState] = useState(true)
const [searchNotification, setSearchNotification] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const [registrations, setRegistrations] = useState<T_RegistrationItem[]>([])
const handleSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQueryParams({ ...queryParams, search: e.target.value })
}
const [queryParams, setQueryParams] = useState<I_QueryParams>({
page: 1,
count: 10,
search: '',
order: undefined
})
const getReg = async () => {
setSearchNotification(false)
setErrorState(false)
try {
const response = await RegistrationsService.getRegistrations(queryParams)
if (response.status === 200) {
response.data.Data.length === 0
? setSearchNotification(true)
: (setRegistrations(response.data.Data),
setTotalCount(response.data.Total))
}
} catch (error) {
setErrorState(true)
}
setLoadingState(false)
}
useEffect(() => {
getReg()
}, [queryParams])
const [activeColumns, setActiveColumns] = useState<T_ColumnsState>({
RegNum: { name: 'Номер поставки', status: true },
ContNum: { name: 'Номер контракта', status: true },
Email: { name: 'Эл. почта', status: true },
Count: { name: 'Кол-во активаций', status: true },
EndDate: { name: 'Дата окончания', status: true },
CreatedDate: { name: 'Дата создания', status: false },
UpdatedDate: { name: 'Дата изменения', status: true },
Enabled: { name: 'Статус', status: true }
})
const [checkedColumns, setCheckedColumns] = useState<number[]>([])
const [selectAll, setSelectAll] = useState<boolean>(false)
const [notChecked, setNotChecked] = useState<number[]>([])
useEffect(() => {
!errorState && (
notChecked.length === registrations.length &&
(setSelectAll(false), setNotChecked([]))
)
}, [notChecked])
const groupDelete = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.deleteRegistration(item)
)
await Promise.all(deletePromises)
getReg()
}
const groupStart = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.onRegistration(item)
)
await Promise.all(deletePromises)
getReg()
}
const groupStop = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.offRegistration(item)
)
await Promise.all(deletePromises)
getReg()
}
const [newReg, setNewReg] = useState<T_NewRegistration>({
RegNum: '',
ContNum: '',
Email: '',
Count: '',
EndDate: ''
})
const [modal, setModal] = useState(false)
useEffect(() => {
setNewReg({
RegNum: '',
ContNum: '',
Email: '',
Count: '',
EndDate: ''
})
}, [modal])
useEffect(() => {
getReg()
}, [])
return (
<>
{loadingState ? (
<RegLoaderContainer>
<Loader />
</RegLoaderContainer>) : errorState ? (<RegLoaderContainer>
<h2>Сервер недоступен</h2>
</RegLoaderContainer>
) : (
<RegContainer>
{registrations.length === 0 && queryParams.search === '' ? (
<RegNotification>
<h2>Активных лицензий нет</h2>
<MainButton onClick={() => setModal(true)}>
Добавить лицензию
</MainButton>
</RegNotification>
) : (
<>
<h2>Система управления и контроля ОСГОС</h2>
<SearchInput
value={queryParams.search}
onChange={handleSearchQueryChange}
placeholder='Введите номер контракта, поставки или email'
/>
<>
{searchNotification ? (
<SearchNotification>
<h3>Ничего не найдено</h3>
</SearchNotification>
) : (
<>
{(selectAll || checkedColumns.length !== 0) && (
<RegSelectNotification>
<h3>
{selectAll
? `Выбранно: ${totalCount - notChecked.length}`
: checkedColumns.length !== 0 &&
`Выбранно: ${checkedColumns.length}`}
</h3>
<div className='menu'>
<img
src='/src/images/play.svg'
alt=''
onClick={groupStart}
/>
<img
src='/src/images/stop.svg'
alt=''
onClick={groupStop}
/>
<img
src='/src/images/trash.svg'
alt=''
onClick={groupDelete}
/>
</div>
</RegSelectNotification>
)}
<RegTable
selectAll={selectAll}
notChecked={notChecked}
setNotChecked={setNotChecked}
setSelectAll={setSelectAll}
checkedColumns={checkedColumns}
setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns}
setActiveColumns={setActiveColumns}
registrations={registrations}
getReg={getReg}
queryParams={queryParams}
setQueryParams={setQueryParams}
/>
<div className='bottom-menu'>
<Pagination
queryParams={queryParams}
setQueryParams={setQueryParams}
totalCount={totalCount}
/>
<MainButton onClick={() => setModal(true)}>
Добавить
</MainButton>
</div>
</>
)}
</>
</>
)}
<NewRegModal
modal={modal}
setModal={setModal}
newReg={newReg}
setNewReg={setNewReg}
getReg={getReg}
/>
</RegContainer>
)}
</>
)
}

View File

@ -0,0 +1,109 @@
import { useEffect, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Modal from 'src/components/UI/modal/Modal'
import styled from 'styled-components'
import { T_NewRegistration } from '../types'
type Props = {
modal: boolean
setModal: (param: boolean) => void
registration: T_NewRegistration
editReg: (param: T_NewRegistration) => void
}
const ModalContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
width: 440px;
.buttonBlock {
display: flex;
gap: 20px;
}
`
export default function EditRegModal({
modal,
setModal,
registration,
editReg
}: Props) {
const [editRegData, setEditRegData] = useState(registration)
const [errorMessage, setErrorMessage] = useState(false)
const handleContNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...editRegData, ContNum: e.target.value })
}
const handleRegNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...editRegData, RegNum: e.target.value })
}
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...editRegData, Email: e.target.value })
}
const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...editRegData, Count: e.target.value })
}
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...editRegData, EndDate: e.target.value })
}
useEffect(() => {
function formatDate(isoDateString: string): string {
const date = new Date(isoDateString)
const year = date.getFullYear()
const month = ('0' + (date.getMonth() + 1)).slice(-2)
const day = ('0' + date.getDate()).slice(-2)
return `${year}-${month}-${day}`
}
const formattedDate = formatDate(editRegData.EndDate)
setEditRegData({ ...editRegData, EndDate: formattedDate })
}, [])
return (
<Modal modal={modal} setModal={setModal}>
<ModalContainer>
<MainInput
placeholder='Номер контракта'
value={editRegData.ContNum}
onChange={handleContNumChange}
/>
<MainInput
placeholder='Номер поставки'
value={editRegData.RegNum}
onChange={handleRegNumChange}
/>
<MainInput
placeholder='Email'
value={editRegData.Email}
onChange={handleEmailChange}
/>
<MainInput
placeholder='Кол-во активаций'
value={editRegData.Count}
onChange={handleCountChange}
/>
<MainInput
placeholder='Дата окончания'
value={editRegData.EndDate}
onChange={handleEndDateChange}
type='date'
/>
<div className='buttonBlock'>
<MainButton
color='secondary'
fullWidth={true}
onClick={() => setModal(false)}
>
Отмена
</MainButton>
<MainButton fullWidth={true} onClick={() => editReg(editRegData)}>
Сохранить
</MainButton>
</div>
</ModalContainer>
</Modal>
)
}

View File

@ -0,0 +1,139 @@
import { useEffect, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Modal from 'src/components/UI/modal/Modal'
import RegistrationsService from 'src/services/registrationsServices'
import styled from 'styled-components'
import { T_NewRegistration } from '../types'
const ModalContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
width: 440px;
.buttonBlock {
display: flex;
gap: 20px;
}
`
const ErrorMessageContent = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 20px;
`
const ErrorMessageContainer = styled.div`
z-index: 3;
`
type Props = {
modal: boolean
setModal: (param: boolean) => void
newReg: T_NewRegistration
setNewReg: (param: T_NewRegistration) => void
getReg: () => void
}
export default function NewRegModal({
modal,
setModal,
newReg,
setNewReg,
getReg
}: Props) {
const [errorMessage, setErrorMessage] = useState(false)
const CreateReg = async () => {
const response = await RegistrationsService.createRegistration(newReg)
if (response.status === 406) {
function formatDate(isoDateString: string): string {
const date = new Date(isoDateString)
const year = date.getFullYear()
const month = ('0' + (date.getMonth() + 1)).slice(-2)
const day = ('0' + date.getDate()).slice(-2)
return `${year}-${month}-${day}`
}
const formattedDate = formatDate(newReg.EndDate)
setNewReg({ ...newReg, EndDate: formattedDate, Count: `${newReg.Count}` })
setErrorMessage(true)
} else {
setModal(false)
getReg()
}
}
const handleContNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewReg({ ...newReg, ContNum: e.target.value })
}
const handleRegNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewReg({ ...newReg, RegNum: e.target.value })
}
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewReg({ ...newReg, Email: e.target.value })
}
const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewReg({ ...newReg, Count: e.target.value })
}
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewReg({ ...newReg, EndDate: e.target.value })
}
return (
<>
<Modal modal={modal} setModal={setModal}>
<ModalContainer>
<MainInput
placeholder='Номер контракта'
value={newReg.ContNum}
onChange={handleContNumChange}
/>
<MainInput
placeholder='Номер поставки'
value={newReg.RegNum}
onChange={handleRegNumChange}
/>
<MainInput
placeholder='Email'
value={newReg.Email}
onChange={handleEmailChange}
/>
<MainInput
placeholder='Кол-во активаций'
value={typeof newReg.Count === 'string' ? newReg.Count : ''}
onChange={handleCountChange}
/>
<MainInput
placeholder='Дата окончания'
value={newReg.EndDate}
onChange={handleEndDateChange}
type='date'
/>
<div className='buttonBlock'>
<MainButton
color='secondary'
fullWidth={true}
onClick={() => setModal(false)}
>
Отмена
</MainButton>
<MainButton fullWidth={true} onClick={CreateReg}>
Создать
</MainButton>
</div>
</ModalContainer>
</Modal>
<ErrorMessageContainer>
<Modal modal={errorMessage} setModal={setErrorMessage}>
<ErrorMessageContent>
<h3>Номер контракта или поставки уже существует</h3>
<MainButton onClick={() => setErrorMessage(false)}>Ok</MainButton>
</ErrorMessageContent>
</Modal>
</ErrorMessageContainer>
</>
)
}

View File

@ -0,0 +1,191 @@
import { useState } from 'react'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox'
import Table from 'src/components/UI/table/Table'
import { I_QueryParams } from 'src/services/type'
import styled, { css } from 'styled-components'
import { T_ColumnsState, T_RegistrationItem } from '../types'
import RegTableItem from './RegTableItem'
interface I_TableHeaderItem {
active: boolean
state: boolean
}
const Item = styled.div`
display: flex;
gap: 10px;
cursor: pointer;
`
const TableHeaderItem = styled.div<I_TableHeaderItem>`
display: flex;
align-items: center;
cursor: pointer;
img {
${(props) =>
props.state &&
css`
transform: rotate(180deg);
`}
${(props) =>
props.active
? css`
display: block;
`
: css`
display: none;
`}
}
`
type Props = {
selectAll: boolean
notChecked: number[]
setNotChecked: (param: any) => void
setSelectAll: (param: any) => void
checkedColumns: number[]
setCheckedColumns: (param: any) => void
activeColumns: T_ColumnsState
setActiveColumns: (param: any) => void
registrations: T_RegistrationItem[]
getReg: () => void
queryParams: I_QueryParams
setQueryParams: (param: I_QueryParams) => void
}
export default function RegTable({
selectAll,
notChecked,
setNotChecked,
setSelectAll,
checkedColumns,
setCheckedColumns,
activeColumns,
setActiveColumns,
registrations,
getReg,
queryParams,
setQueryParams
}: Props) {
const [contextMenuState, setContextMenuState] = useState(false)
const [activeSort, setActiveSort] = useState('')
const [sortState, setSortState] = useState(false)
const sort = (value: string) => {
const sortOptions: { [key: string]: string } = {
'Номер поставки': 'reg',
'Номер контракта': 'cont',
'Эл. почта': 'email',
'Кол-во активаций': 'count',
'Дата окончания': 'end',
'Дата изменения': 'updated',
'Статус': 'enabled'
};
const sortParam = sortOptions[value];
if (!sortParam) return;
setActiveSort(value);
const newOrder = queryParams.order === sortParam ? `!${sortParam}` : sortParam;
setQueryParams({ ...queryParams, order: newOrder });
setSortState(queryParams.order === sortParam);
};
return (
<Table>
<thead>
<tr>
<th>
<Checkbox
value={
selectAll
? notChecked.length === 0
? 'ok'
: 'minus'
: checkedColumns.length === 0
? 'off'
: 'minus'
}
onClick={() => {
selectAll
? (setSelectAll(false),
setNotChecked([]),
setCheckedColumns([]))
: checkedColumns.length === 0
? (setSelectAll(true), setNotChecked([]))
: setCheckedColumns([])
}}
/>
</th>
{Object.entries(activeColumns).map(
([index, item]) =>
item.status === true && (
<th>
<TableHeaderItem
key={index}
active={activeSort === item.name}
state={sortState}
onClick={() => sort(item.name)}
>
<img src='/src/images/arrow-for-table.svg' alt='' />
{item.name}
</TableHeaderItem>
</th>
)
)}
<th>
<ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
positionTop
>
<div className='target'>
<img
src='/src/images/params.svg'
alt=''
onClick={() => setContextMenuState(!contextMenuState)}
/>
</div>
<div className='content'>
{Object.entries(activeColumns).map(([key, item]) => (
<Item
key={key}
onClick={() => {
const updatedActiveColumns = { ...activeColumns }
updatedActiveColumns[key].status =
!updatedActiveColumns[key].status
setActiveColumns(updatedActiveColumns)
}}
>
<Checkbox
value={item.status ? 'ok' : 'off'}
onClick={() => {}}
/>
<p>{item.name}</p>
</Item>
))}
</div>
</ContextMenu>
</th>
</tr>
</thead>
<tbody>
{registrations.map((registration) => (
<RegTableItem
registration={registration}
selectAll={selectAll}
notChecked={notChecked}
checkedColumns={checkedColumns}
setNotChecked={setNotChecked}
setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns}
getReg={getReg}
/>
))}
</tbody>
</Table>
)
}

View File

@ -0,0 +1,211 @@
import { useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox'
import Modal from 'src/components/UI/modal/Modal'
import RegistrationsService from 'src/services/registrationsServices'
import styled from 'styled-components'
import { T_ColumnsState, T_NewRegistration, T_RegistrationItem } from '../types'
import EditRegModal from './EditRegModal'
type Props = {
registration: T_RegistrationItem
selectAll: boolean
notChecked: number[]
checkedColumns: number[]
setNotChecked: (param: any) => void
setCheckedColumns: (param: any) => void
activeColumns: T_ColumnsState
getReg: () => void
}
const Item = styled.div`
display: flex;
gap: 10px;
cursor: pointer;
`
const DeleteNotification = styled.div`
.button-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
`
export default function RrgTableItem ({
registration,
selectAll,
notChecked,
checkedColumns,
setNotChecked,
setCheckedColumns,
activeColumns,
getReg
}: Props) {
const [contextMenuState, setContextMenuState] = useState(false)
const [deleteNotificationState, setDeleteNotificationState] = useState(false)
const [editModalState, setEditModalState] = useState(false)
const deleteReg = async () => {
setContextMenuState(false)
const response = await RegistrationsService.deleteRegistration(
registration.Id
)
if (response.status === 200) {
getReg()
}
setDeleteNotificationState(false)
}
const onReg = async () => {
setContextMenuState(false)
const response = await RegistrationsService.onRegistration(registration.Id)
if (response.status === 200) {
getReg()
}
}
const offReg = async () => {
setContextMenuState(false)
const response = await RegistrationsService.offRegistration(registration.Id)
if (response.status === 200) {
getReg()
}
}
const editReg = async (reg: T_NewRegistration) => {
setContextMenuState(false)
const response = await RegistrationsService.editRegistration(
registration.Id,
reg
)
if (response.status === 200) {
getReg()
}
setEditModalState(false)
}
return (
<>
<tr key={registration.Id}>
<td>
<Checkbox
value={
selectAll
? notChecked.includes(registration.Id)
? 'off'
: 'ok'
: checkedColumns.includes(registration.Id)
? 'ok'
: 'off'
}
onClick={() => {
selectAll
? notChecked.includes(registration.Id)
? setNotChecked(
notChecked.filter((item) => item !== registration.Id)
)
: setNotChecked([...notChecked, registration.Id])
: checkedColumns.includes(registration.Id)
? (setCheckedColumns(
checkedColumns.filter((item) => item !== registration.Id)
),
setNotChecked([...notChecked, registration.Id]))
: setCheckedColumns([...checkedColumns, registration.Id])
}}
/>
</td>
{Object.entries(activeColumns).map(([key, item]) =>
activeColumns[key as keyof typeof activeColumns].status ? (
<td key={key}>
<p>
{' '}
{key === 'Enabled' ? (
registration[key as keyof typeof registration] ? (
<img src='/src/images/on.svg' />
) : (
<img src='/src/images/off.svg' />
)
) : (
registration[key as keyof typeof registration]
)}
</p>
</td>
) : null
)}
<td>
<ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
>
<div className='target'>
<img
src='/src/images/menu.svg'
alt=''
onClick={() => setContextMenuState(!contextMenuState)}
/>
</div>
<div className='content'>
<Item
onClick={() => {
setEditModalState(true)
setContextMenuState(false)
}}
>
<img src='/src/images/edit.svg' alt='' />
<p>Редактировать</p>
</Item>
<Item
onClick={() => {
setContextMenuState(false)
setDeleteNotificationState(true)
}}
>
<img src='/src/images/trash.svg' alt='' />
<p>Удалить</p>
</Item>
{registration.Enabled ? (
<Item onClick={offReg}>
<img src='/src/images/stop.svg' alt='' />
<p>Выключить</p>
</Item>
) : (
<Item onClick={onReg}>
<img src='/src/images/play.svg' alt='' />
<p>Включить</p>
</Item>
)}
</div>
</ContextMenu>
</td>
<Modal
modal={deleteNotificationState}
setModal={setDeleteNotificationState}
>
<DeleteNotification>
<h3>Вы действительно хотите удалить лицензию?</h3>
<div className='button-container'>
<MainButton
color={'secondary'}
fullWidth={true}
onClick={() => setDeleteNotificationState(false)}
>
Отмена
</MainButton>
<MainButton fullWidth={true} onClick={() => deleteReg()}>
Удалить
</MainButton>
</div>
</DeleteNotification>
</Modal>
<EditRegModal
modal={editModalState}
setModal={setEditModalState}
registration={registration}
editReg={editReg}
/>
</tr>
</>
)
}

View File

@ -0,0 +1,68 @@
import styled from 'styled-components'
export const RegLoaderContainer = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const RegContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
align-items: start;
padding: 40px 20px;
.bottom-menu {
display: flex;
align-items: start;
width: 100%;
justify-content: space-between;
}
`
export const RegNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
`
export const SearchNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const RegSelectNotification = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
${(props) => props.theme.mainShadow}
border-radius: 12px;
.menu {
display: flex;
gap: 10px;
}
img {
padding: 5px;
cursor: pointer;
border-radius: 6px;
transition: 300ms;
&:hover {
background: ${(props) => props.theme.focusColor};
transform: scale(1.1);
}
}
`

View File

@ -0,0 +1,28 @@
export type T_NewRegistration = {
RegNum: string
ContNum: string
Email: string
Count: string | number
EndDate: string
}
export interface T_RegistrationItem {
Id: number;
RegNum: string;
ContNum: string;
Email: string;
Count: number;
EndDate: string;
CreatedDate: string;
UpdatedDate: string;
Enabled: boolean;
}
export type T_Column = {
name: string
status: boolean
}
export type T_ColumnsState = {
[key: string]: T_Column
}

View File

@ -0,0 +1,215 @@
import React from 'react'
import { useEffect, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import SearchInput from 'src/components/UI/input/SearchInput'
import Loader from 'src/components/UI/loader/Loader'
import Pagination from 'src/components/UI/table/pagination/Pagination'
import RepositoryService from 'src/services/repositoryServices'
import { I_QueryParams } from 'src/services/type'
import NewRepModal from './blocks/NewRepModal'
import RepTable from './blocks/RepTable'
import {
RepContainer,
RepLoaderContainer,
RepNotification,
RepSelectNotification,
SearchNotification
} from './styles'
import { T_ColumnsState, T_RepositoryItem } from './types'
type Props = {}
export default function Repository({}: Props) {
const [firstLoadingState, setFirstLoadingState] = useState(true)
const [loadingState, setLoadingState] = useState(false)
const [errorState, setErrorState] = useState(false)
const [searchNotification, setSearchNotification] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const [Repository, setRepository] = useState<T_RepositoryItem[]>([])
const handleSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQueryParams({ ...queryParams, search: e.target.value })
}
const [queryParams, setQueryParams] = useState<I_QueryParams>({
page: 1,
count: 10,
search: '',
order: undefined
})
const getRep = async () => {
if (!firstLoadingState) setLoadingState(true)
setErrorState(false)
try {
const response = await RepositoryService.getRepository(queryParams)
if (response.status === 200) {
response.data.Data.length === 0
? setSearchNotification(true)
: (setRepository(response.data.Data),
setTotalCount(response.data.Total),
setSearchNotification(false))
}
} catch (error) {
setErrorState(true)
}
setLoadingState(false)
setFirstLoadingState(false)
}
useEffect(() => {
getRep()
}, [])
useEffect(() => {
getRep()
}, [queryParams])
const [activeColumns, setActiveColumns] = useState<T_ColumnsState>({
Name: { name: 'Название', status: true },
Version: { name: 'Версия', status: true },
Description: { name: 'Полное описание', status: true },
ShortDescription: { name: 'Краткое описание', status: true },
Category: { name: 'Категория', status: true },
UpdatedDate: { name: 'Дата изменения', status: true },
Enabled: { name: 'Статус', status: true }
})
const [checkedColumns, setCheckedColumns] = useState<number[]>([])
const [selectAll, setSelectAll] = useState<boolean>(false)
const [notChecked, setNotChecked] = useState<number[]>([])
useEffect(() => {
!errorState ||
loadingState ||
(firstLoadingState &&
notChecked.length === Repository .length &&
(setSelectAll(false), setNotChecked([])))
}, [notChecked])
const [modal, setModal] = useState(false)
const groupDelete = async () => {
const deletePromises = checkedColumns.map((item) =>
RepositoryService.deleteRepository(item)
)
await Promise.all(deletePromises)
getRep()
}
const groupStart = async () => {
const deletePromises = checkedColumns.map((item) =>
RepositoryService.onRepository(item)
)
await Promise.all(deletePromises)
getRep()
}
const groupStop = async () => {
const deletePromises = checkedColumns.map((item) =>
RepositoryService.offRepository(item)
)
await Promise.all(deletePromises)
getRep()
}
return (
<>
{loadingState ? (
<RepLoaderContainer>
<Loader />
</RepLoaderContainer>
) : (
<RepContainer>
{Repository.length === 0 && queryParams.search === '' ? (
<RepNotification>
<h2>Активных пакетов нет</h2>
<MainButton onClick={() => setModal(true)}>
Добавить пакет
</MainButton>
</RepNotification>
) : (
<>
<h2>Система управления и контроля ОСГОС</h2>
<SearchInput
value={queryParams.search}
onChange={handleSearchQueryChange}
placeholder='Введите название пакета'
/>
<>
{searchNotification ? (
<SearchNotification>
<h3>Ничего не найдено</h3>
</SearchNotification>
) : (
<>
{(selectAll || checkedColumns.length !== 0) && (
<RepSelectNotification>
<h3>
{selectAll
? `Выбрано: ${totalCount - notChecked.length}`
: checkedColumns.length !== 0 &&
`Выбрано: ${checkedColumns.length}`}
</h3>
<div className='menu'>
<img
src='/src/images/play.svg'
alt=''
onClick={groupStart}
/>
<img
src='/src/images/stop.svg'
alt=''
onClick={groupStop}
/>
<img
src='/src/images/trash.svg'
alt=''
onClick={groupDelete}
/>
</div>
</RepSelectNotification>
)}
<RepTable
selectAll={selectAll}
notChecked={notChecked}
setNotChecked={setNotChecked}
setSelectAll={setSelectAll}
checkedColumns={checkedColumns}
setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns}
setActiveColumns={setActiveColumns}
repository={Repository}
getRep={getRep}
queryParams={queryParams}
setQueryParams={setQueryParams}
/>
<div className='bottom-menu'>
<Pagination
queryParams={queryParams}
setQueryParams={setQueryParams}
totalCount={totalCount}
/>
<MainButton onClick={() => setModal(true)}>
Добавить
</MainButton>
</div>
</>
)}
</>
</>
)}
</RepContainer>
)}
<NewRepModal modal={modal} setModal={setModal} getRep={getRep} />
</>
)
}

View File

@ -0,0 +1,226 @@
import { useEffect, useRef, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Modal from 'src/components/UI/modal/Modal'
import Textarea from 'src/components/UI/input/Textarea'
import RepositoryService from 'src/services/repositoryServices'
import styled from 'styled-components'
import { T_NewRepository, T_RepositoryItem } from '../types'
const NewRepErrorMessageContainer = styled.div`
position: relative;
z-index: 4;
`
const NewRepErrorMessage = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 20px;
`
type Props = {
modal: boolean
setModal: (param: boolean) => void
repository: T_RepositoryItem
getRep: () => void
}
const ModalContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
width: 440px;
.buttonBlock {
display: flex;
gap: 20px;
}
`
const NewRepFileBlock = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
input {
display: none;
}
p {
font-size: 18px;
}
span {
font-weight: 600;
}
`
export default function EditRepModal({ modal, setModal, repository, getRep }: Props){
const [error, setError] = useState({
state: false,
message: ''
})
const [loading, setLoading] = useState(false)
const setErrorState = (value: boolean) => {
setError({ ...error, state: value })
}
const [editRepData, setEditRepData] = useState<T_NewRepository>({
Name: repository.Name,
Description: repository.Description,
ShortDescription: repository.ShortDescription,
Version: repository.Version,
Category: repository.Category
})
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRepData({ ...editRepData, Name: e.target.value })
}
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditRepData({ ...editRepData, Description: e.target.value })
}
const handleShortDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRepData({ ...editRepData, ShortDescription: e.target.value })
}
const handleVersionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRepData({ ...editRepData, Version: e.target.value })
}
const handleCategoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRepData({ ...editRepData, Category: e.target.value })
}
const [formValid, setFormValid] = useState(false)
useEffect(() => {
setFormValid(
editRepData.Name !== '' &&
editRepData.Description !== '' &&
editRepData.ShortDescription !== '' &&
editRepData.Version !== '' &&
editRepData.Category !== ''
)
}, [editRepData])
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const handleFileChange = () => {
const file = fileInputRef.current?.files?.[0]
if (file) {
setSelectedFile(file)
}
}
const handleFileClick = () => {
fileInputRef.current?.click()
}
const updateRep = async () => {
setLoading(true)
try {
if (
!(
editRepData.Category === repository.Category &&
editRepData.Description === repository.Description &&
editRepData.Name === repository.Name &&
editRepData.ShortDescription === repository.ShortDescription &&
editRepData.Version === repository.Version
)
) {
const response = await RepositoryService.editRepository(repository.Id, editRepData)
if (response.status === 406) {
setError({ state: true, message: response.data })
setLoading(false)
return
}
}
if (repository.Url !== selectedFile && selectedFile instanceof File) {
const response = await RepositoryService.addFile(selectedFile, repository.Id)
if (response.status === 406) {
setError({ state: true, message: response.data })
setLoading(false)
return
}
}
setLoading(false)
setModal(false)
getRep()
} catch (error) {}
}
return (
<>
<Modal modal={modal} setModal={setModal}>
<ModalContainer>
<h2>Изменение пакета в репозитории</h2>
<MainInput
placeholder='Название'
value={editRepData.Name}
onChange={handleNameChange}
/>
<MainInput
placeholder='Версия'
value={editRepData.Version}
onChange={handleVersionChange}
/>
<MainInput
placeholder='Краткое описание'
value={editRepData.Description}
onChange={handleShortDescriptionChange}
/>
<Textarea
placeholder='Полное описание'
value={editRepData.Description}
onChange={handleDescriptionChange}
/>
<MainInput
placeholder='Категория'
value={editRepData.Category}
onChange={handleCategoryChange}
/>
<div>
<NewRepFileBlock>
<input
ref={fileInputRef}
type='file'
accept='.deb'
onChange={handleFileChange}
/>
{selectedFile ? (
<p>
<span>Выбранный файл: </span>
{selectedFile.name}
</p>
) : (
<p>
<span>Файл не выбран</span>
</p>
)}
<MainButton onClick={handleFileClick}>Выбрать файл</MainButton>
</NewRepFileBlock>
</div>
<div className='buttonBlock'>
<MainButton
color='secondary'
fullWidth
onClick={() => setModal(false)}
>
Отмена
</MainButton>
<MainButton fullWidth onClick={updateRep} disabled={!formValid}>
Сохранить
</MainButton>
</div>
</ModalContainer>
</Modal>
<NewRepErrorMessageContainer>
<Modal modal={error.state} setModal={setErrorState}>
<NewRepErrorMessage>
<h3>{error.message}</h3>
<MainButton onClick={() => setErrorState(false)}>Ok</MainButton>
</NewRepErrorMessage>
</Modal>
</NewRepErrorMessageContainer>
</>
)
}

View File

@ -0,0 +1,211 @@
import { useEffect, useRef, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Modal from 'src/components/UI/modal/Modal'
import Textarea from 'src/components/UI/input/Textarea'
import RepositoryService from 'src/services/repositoryServices'
import styled from 'styled-components'
import { T_NewRepository } from '../types'
const NewRepErrorMessageContainer = styled.div`
position: relative;
z-index: 4;
`
const NewRepErrorMessage = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 20px;
`
const NewAppFileBlock = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
input {
display: none;
}
p {
font-size: 18px;
}
span {
font-weight: 600;
}
`
const ModalContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
width: 440px;
.buttonBlock {
display: flex;
gap: 20px;
}
`
type Props = {
modal: boolean
setModal: (param: boolean) => void
getRep: () => void
}
export default function NewRepModal({ modal, setModal, getRep }: Props) {
const [newRep, setNewRep] = useState<T_NewRepository>({
Name: '',
Description: '',
ShortDescription: '',
Version: '',
Category: ''
})
const [selectedFile, setSelectedFile] = useState<File | null>(null)
useEffect(() => {
setNewRep({
Name: '',
Description: '',
ShortDescription: '',
Version: '',
Category: ''
})
setSelectedFile(null)
}, [modal])
const [formValid, setFormValid] = useState(false)
useEffect(() => {
setFormValid(
newRep.Name !== '' &&
newRep.Description !== '' &&
newRep.ShortDescription !== '' &&
newRep.Version !== '' &&
newRep.Category !== ''
)
}, [newRep])
const [errorState, setErrorState] = useState(false)
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewRep({ ...newRep, Name: e.target.value })
}
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setNewRep({ ...newRep, Description: e.target.value })
}
const handleShortDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewRep({ ...newRep, ShortDescription: e.target.value })
}
const handleVersionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewRep({ ...newRep, Version: e.target.value })
}
const handleCategoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewRep({ ...newRep, Category: e.target.value })
}
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = () => {
const file = fileInputRef.current?.files?.[0]
if (file) {
setSelectedFile(file)
}
}
const handleFileClick = () => {
fileInputRef.current?.click()
}
const repCreate = async () => {
try {
const response = await RepositoryService.createRep(newRep)
if (response.status === 200) {
selectedFile !== null &&
(await RepositoryService.addFile(selectedFile, response.data.Id))
} else if (response.status === 406) {
setErrorState(true)
}
setModal(false)
getRep()
} catch (error) {
console.log(error)
}
}
return (
<>
<Modal modal={modal} setModal={setModal}>
<ModalContainer>
<h2>Внесение пакета в репозиторий</h2>
<MainInput
placeholder='Название'
value={newRep.Name}
onChange={handleNameChange}
/>
<MainInput
placeholder='Версия'
value={newRep.Version}
onChange={handleVersionChange}
/>
<MainInput
placeholder='Краткое описание'
value={newRep.ShortDescription}
onChange={handleShortDescriptionChange}
/>
<Textarea
placeholder='Полное описание'
value={newRep.Description}
onChange={handleDescriptionChange}
/>
<MainInput
placeholder='Категория'
value={newRep.Category}
onChange={handleCategoryChange}
/>
<div>
<NewAppFileBlock>
<input
ref={fileInputRef}
type='file'
accept='.deb'
onChange={handleFileChange}
/>
{selectedFile ? (
<p>
<span>Выбранный файл: </span>
{selectedFile.name}
</p>
) : (
<p>
<span>Файл не выбран</span>
</p>
)}
<MainButton onClick={handleFileClick}>Выбрать файл</MainButton>
</NewAppFileBlock>
</div>
<div className='buttonBlock'>
<MainButton
color='secondary'
fullWidth
onClick={() => setModal(false)}
>
Отмена
</MainButton>
<MainButton fullWidth onClick={repCreate} disabled={!formValid}>
Создать
</MainButton>
</div>
</ModalContainer>
</Modal>
<NewRepErrorMessageContainer>
<Modal modal={errorState} setModal={setErrorState}>
<NewRepErrorMessage>
<h3>Имя и версия уже существует</h3>
<MainButton onClick={() => setErrorState(false)}>Ok</MainButton>
</NewRepErrorMessage>
</Modal>
</NewRepErrorMessageContainer>
</>
)
}

View File

@ -0,0 +1,188 @@
import { useState } from 'react'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox'
import Table from 'src/components/UI/table/Table'
import { I_QueryParams } from 'src/services/type'
import styled, { css } from 'styled-components'
import { T_ColumnsState, T_RepositoryItem } from '../types'
import RepTableItem from './RepTableItem'
interface I_TableHeaderItem {
active: boolean
state: boolean
}
const Item = styled.div`
display: flex;
gap: 10px;
cursor: pointer;
`
const TableHeaderItem = styled.div<I_TableHeaderItem>`
display: flex;
align-items: center;
cursor: pointer;
img {
${(props) =>
props.state &&
css`
transform: rotate(180deg);
`}
${(props) =>
props.active
? css`
display: block;
`
: css`
display: none;
`}
}
`
type Props = {
selectAll: boolean
notChecked: number[]
setNotChecked: (param: any) => void
setSelectAll: (param: any) => void
checkedColumns: number[]
setCheckedColumns: (param: any) => void
activeColumns: T_ColumnsState
setActiveColumns: (param: any) => void
repository: T_RepositoryItem[]
getRep: () => void
queryParams: I_QueryParams
setQueryParams: (param: I_QueryParams) => void
}
export default function RepTable({
selectAll,
notChecked,
setNotChecked,
setSelectAll,
checkedColumns,
setCheckedColumns,
activeColumns,
setActiveColumns,
repository,
getRep,
queryParams,
setQueryParams
}: Props) {
const [contextMenuState, setContextMenuState] = useState(false)
const [activeSort, setActiveSort] = useState('')
const [sortState, setSortState] = useState(false)
const sort = (value: string) => {
const sortOptions: { [key: string]: string } = {
'Статус': 'enabled',
'Название': 'string',
'Версия': 'string',
'Описание': 'string',
'Дата изменения': 'updated',
};
const sortParam = sortOptions[value];
if (!sortParam) return;
setActiveSort(value);
const newOrder = queryParams.order === sortParam ? `!${sortParam}` : sortParam;
setQueryParams({ ...queryParams, order: newOrder });
setSortState(queryParams.order === sortParam);
};
return (
<Table>
<thead>
<tr>
<th>
<Checkbox
value={
selectAll
? notChecked.length === 0
? 'ok'
: 'minus'
: checkedColumns.length === 0
? 'off'
: 'minus'
}
onClick={() => {
selectAll
? (setSelectAll(false),
setNotChecked([]),
setCheckedColumns([]))
: checkedColumns.length === 0
? (setSelectAll(true), setNotChecked([]))
: setCheckedColumns([])
}}
/>
</th>
{Object.entries(activeColumns).map(
([index, item]) =>
item.status === true && (
<th>
<TableHeaderItem
key={index}
active={activeSort === item.name}
state={sortState}
onClick={() => sort(item.name)}
>
<img src='/src/images/arrow-for-table.svg' alt='' />
{item.name}
</TableHeaderItem>
</th>
)
)}
<th>
<ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
>
<div className='target'>
<img
src='/src/images/params.svg'
alt=''
onClick={() => setContextMenuState(!contextMenuState)}
/>
</div>
<div className='content'>
{Object.entries(activeColumns).map(([key, item]) => (
<Item
key={key}
onClick={() => {
const updatedActiveColumns = { ...activeColumns }
updatedActiveColumns[key].status =
!updatedActiveColumns[key].status
setActiveColumns(updatedActiveColumns)
}}
>
<Checkbox
value={item.status ? 'ok' : 'off'}
onClick={() => {}}
/>
<p>{item.name}</p>
</Item>
))}
</div>
</ContextMenu>
</th>
</tr>
</thead>
<tbody>
{repository.map((repository) => (
<RepTableItem
repository={repository}
selectAll={selectAll}
notChecked={notChecked}
checkedColumns={checkedColumns}
setNotChecked={setNotChecked}
setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns}
getRep={getRep}
/>
))}
</tbody>
</Table>
)
}

View File

@ -0,0 +1,245 @@
import { useState } from 'react'
import { HOST_NAME } from 'src/constants/host'
import { Link } from 'react-router-dom'
import MainButton from 'src/components/UI/button/MainButton'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox'
import Modal from 'src/components/UI/modal/Modal'
import RepositoryService from 'src/services/repositoryServices'
import styled from 'styled-components'
import { T_ColumnsState, T_RepositoryItem } from '../types'
import EditRepModal from './EditRepModal'
type Props = {
repository: T_RepositoryItem
selectAll: boolean
notChecked: number[]
checkedColumns: number[]
setNotChecked: (param: any) => void
setCheckedColumns: (param: any) => void
activeColumns: T_ColumnsState
getRep: () => void
}
const Item = styled.div`
display: flex;
gap: 10px;
cursor: pointer;
`
const DeleteNotification = styled.div`
.button-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
`
export default function ({
repository,
selectAll,
notChecked,
checkedColumns,
setNotChecked,
setCheckedColumns,
activeColumns,
getRep
}: Props) {
const [contextMenuState, setContextMenuState] = useState(false)
const [deleteNotificationState, setDeleteNotificationState] = useState(false)
const [editModalState, setEditModalState] = useState(false)
const deleteRep = async () => {
setContextMenuState(false)
const response = await RepositoryService.deleteRepository(
repository.Id
)
if (response.status === 200) {
getRep()
}
setDeleteNotificationState(false)
}
const onRep = async () => {
setContextMenuState(false)
const response = await RepositoryService.onRepository(repository.Id)
if (response.status === 200) {
getRep()
}
}
const offRep = async () => {
setContextMenuState(false)
const response = await RepositoryService.offRepository(repository.Id)
if (response.status === 200) {
getRep()
}
}
const repRep = async () => {
setContextMenuState(false)
const response = await RepositoryService.RepackReposytory(repository.Id)
if (response.status === 200) {
getRep()
}
}
const depRep = async () => {
setContextMenuState(false)
const response = await RepositoryService.DependenciesReposytory(repository.Id)
if (response.status === 200) {
getRep()
}
}
const dowRep = async () => {
setContextMenuState(false)
const response = await RepositoryService.DownloadFile(repository.Id)
if (response.status === 200) {
getRep()
}
}
return (
<>
<tr key={repository.Id}>
<td>
<Checkbox
value={
selectAll
? notChecked.includes(repository.Id)
? 'off'
: 'ok'
: checkedColumns.includes(repository.Id)
? 'ok'
: 'off'
}
onClick={() => {
selectAll
? notChecked.includes(repository.Id)
? setNotChecked(
notChecked.filter((item) => item !== repository.Id)
)
: setNotChecked([...notChecked, repository.Id])
: checkedColumns.includes(repository.Id)
? (setCheckedColumns(
checkedColumns.filter((item) => item !== repository.Id)
),
setNotChecked([...notChecked, repository.Id]))
: setCheckedColumns([...checkedColumns, repository.Id])
}}
/>
</td>
{Object.entries(activeColumns).map(([key, item]) =>
activeColumns[key as keyof typeof activeColumns].status ? (
<td key={key}>
<p>
{' '}
{key === 'Enabled' ? (
repository[key as keyof typeof repository] ? (
<img src='/src/images/on.svg' />
) : (
<img src='/src/images/off.svg' />
)
) : (
repository[key as keyof typeof repository]
)}
</p>
</td>
) : null
)}
<td>
<ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
>
<div className='target'>
<img
src='/src/images/menu.svg'
alt=''
onClick={() => setContextMenuState(!contextMenuState)}
/>
</div>
<div className='content'>
{repository.Enabled ? (
<Item onClick={offRep}>
<img src='/src/images/stop.svg' alt='' />
<p>Выключить</p>
</Item>
) : (
<Item onClick={onRep}>
<img src='/src/images/play.svg' alt='' />
<p>Включить</p>
</Item>
)}
<Item onClick={repRep}>
<img src='/src/images/reload.svg' alt='' />
<p>Перепаковать</p>
</Item>
<Item onClick={depRep}>
<img src='/src/images/trash.svg' alt='' />
<p>Очистить зависимости</p>
</Item>
{repository.Url !== '' && (
<Link
to={`${HOST_NAME}/${repository.Url}`}
onClick={() => setContextMenuState(false)}
>
<Item onClick={dowRep}>
<img
src='/src/images/download.svg'
alt=''
style={{ width: '20px' }}
/>
<p>Скачать</p>
</Item>
</Link>
)}
<Item onClick={() => (setEditModalState(true), setContextMenuState(false))}>
<img src='/src/images/edit.svg' alt='' />
<p>Редактировать</p>
</Item>
<Item onClick={() => {
setContextMenuState(false)
setDeleteNotificationState(true)
}}
>
<img src='/src/images/trash.svg' alt='' />
<p>Удалить</p>
</Item>
</div>
</ContextMenu>
</td>
<Modal
modal={deleteNotificationState}
setModal={setDeleteNotificationState}
>
<DeleteNotification>
<h3>Вы действительно хотите удалить пакет?</h3>
<div className='button-container'>
<MainButton
color={'secondary'}
fullWidth
onClick={() => setDeleteNotificationState(false)}
>
Отмена
</MainButton>
<MainButton fullWidth onClick={() => deleteRep()}>
Удалить
</MainButton>
</div>
</DeleteNotification>
</Modal>
<EditRepModal
modal={editModalState}
setModal={setEditModalState}
repository={repository}
getRep={getRep}
/>
</tr>
</>
)
}

View File

@ -0,0 +1,68 @@
import styled from 'styled-components'
export const RepLoaderContainer = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const RepContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
align-items: start;
padding: 40px 20px;
.bottom-menu {
display: flex;
align-items: start;
width: 100%;
justify-content: space-between;
}
`
export const RepNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
`
export const SearchNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const RepSelectNotification = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
${(props) => props.theme.mainShadow}
border-radius: 12px;
.menu {
display: flex;
gap: 10px;
}
img {
padding: 5px;
cursor: pointer;
border-radius: 6px;
transition: 300ms;
&:hover {
background: ${(props) => props.theme.focusColor};
transform: scale(1.1);
}
}
`

View File

@ -0,0 +1,31 @@
export type T_NewRepository = {
Name: string
Description: string
ShortDescription: string
Version: string
Category: string
}
export interface T_RepositoryItem {
Id: number;
Name: string;
Description: string;
ShortDescription: string;
Version: string;
DependenciesReceived: boolean;
Arch: "amd64" | "x86" | string;
Category: string;
CreatedDate: string;
UpdatedDate: string;
Url: string|null;
Enabled: boolean;
}
export type T_Column = {
name: string
status: boolean
}
export type T_ColumnsState = {
[key: string]: T_Column
}

243
src/pages/Store/Store.tsx Normal file
View File

@ -0,0 +1,243 @@
import { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import MainButton from 'src/components/UI/button/MainButton'
import SearchInput from 'src/components/UI/input/SearchInput'
import Loader from 'src/components/UI/loader/Loader'
import Pagination from 'src/components/UI/table/pagination/Pagination'
import StoreService from 'src/services/storeServices'
import { I_QueryParams } from 'src/services/type'
import { T_ColumnsState } from '../Registrations/types'
import StoreTable from './blocks/StoreTable'
import NewAppModal from './blocks/newAppModal/NewAppModal'
import {
SearchNotification,
StoreContainer,
StoreLoaderContainer,
StoreNotification,
StoreSelectNotification
} from './styles'
import { T_App } from './type'
type Props = {}
export default function Store({}: Props) {
const [firstLoadingState, setFirstLoadingState] = useState(true)
const [loadingState, setLoadingState] = useState(false)
const [errorState, setErrorState] = useState(false)
const [searchNotification, setSearchNotification] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const [Apps, setApps] = useState<T_App[]>([])
const [queryParams, setQueryParams] = useState<I_QueryParams>({
page: 1,
count: 10,
search: '',
order: undefined
})
const location = useLocation()
const searchParams = new URLSearchParams(location.search)
const navigate = useNavigate()
const handleSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSearchQuery = e.target.value
setQueryParams({ ...queryParams, search: e.target.value, page: 1 })
const searchParams = new URLSearchParams(window.location.search)
if (searchParams.has('page')) {
searchParams.delete('page')
}
searchParams.append('page', '1')
if (searchParams.has('search')) {
searchParams.delete('search')
}
if (newSearchQuery.trim() !== '') {
searchParams.append('search', newSearchQuery.trim())
}
navigate(`?${searchParams.toString()}`)
}
const getApps = async () => {
if (!firstLoadingState) setLoadingState(true)
setErrorState(false)
try {
const response = await StoreService.getApp(queryParams)
if (response.status === 200) {
response.data.Data.length === 0
? setSearchNotification(true)
: (setApps(response.data.Data),
setTotalCount(response.data.Total),
setSearchNotification(false))
}
} catch (error) {
setErrorState(true)
}
setLoadingState(false)
setFirstLoadingState(false)
}
useEffect(() => {
const pageParam = searchParams.get('page')
const countParam = searchParams.get('count')
const searchParam = searchParams.get('search')
const orderParam = searchParams.get('order')
setQueryParams((prevState) => ({
...prevState,
page: pageParam ? parseInt(pageParam, 10) : prevState.page,
count: countParam ? parseInt(countParam, 10) : prevState.count,
search: searchParam || prevState.search,
order: orderParam || prevState.order
}))
getApps()
}, [])
useEffect(() => {
getApps()
}, [queryParams])
const [activeColumns, setActiveColumns] = useState<T_ColumnsState>({
Enabled: { name: 'Состояние', status: true },
Name: { name: 'Название', status: true },
IconUrl: { name: 'Иконка', status: true },
Version: { name: 'Версия', status: true },
ShortDescription: { name: 'Краткое описание', status: true },
UpdatedDate: { name: 'Дата обновления', status: true },
Category: { name: 'Категория', status: false },
CreatedDate: { name: 'Дата создания', status: false }
})
const [checkedRows, setCheckedRows] = useState<number[]>([])
const [selectAll, setSelectAll] = useState<boolean>(false)
const [notChecked, setNotChecked] = useState<number[]>([])
useEffect(() => {
!errorState ||
loadingState ||
(firstLoadingState &&
notChecked.length === Apps.length &&
(setSelectAll(false), setNotChecked([])))
}, [notChecked])
const [modal, setModal] = useState(false)
const groupDelete = async () => {
const Promises = checkedRows.map((item) => StoreService.delete(item))
await Promise.all(Promises)
getApps()
}
const groupStart = async () => {
const Promises = checkedRows.map((item) =>
StoreService.interaction('enable', item)
)
await Promise.all(Promises)
getApps()
}
const groupStop = async () => {
const Promises = checkedRows.map((item) =>
StoreService.interaction('disable', item)
)
await Promise.all(Promises)
getApps()
}
return (
<>
{firstLoadingState ? (
<StoreLoaderContainer>
<Loader />
</StoreLoaderContainer>
) : errorState ? (
<StoreLoaderContainer>
<h2>Сервер недоступен</h2>
</StoreLoaderContainer>
) : (
<StoreContainer>
{Apps.length === 0 && queryParams.search === '' ? (
<StoreNotification>
<h2>Список приложений пуст</h2>
<MainButton onClick={() => setModal(true)}>
Добавить приложение
</MainButton>
</StoreNotification>
) : (
<>
<h2>Система управления и контроля ОСГОС</h2>
<SearchInput
value={queryParams.search}
onChange={handleSearchQueryChange}
placeholder='Введите номер контракта, поставки или email'
/>
<>
{searchNotification ? (
<SearchNotification>
<h3>Ничего не найдено</h3>
</SearchNotification>
) : (
<>
{(selectAll || checkedRows.length !== 0) && (
<StoreSelectNotification>
<h3>
{selectAll
? `Выбрано: ${totalCount - notChecked.length}`
: checkedRows.length !== 0 &&
`Выбрано: ${checkedRows.length}`}
</h3>
<div className='menu'>
<img
src='/src/images/play.svg'
alt=''
onClick={groupStart}
/>
<img
src='/src/images/stop.svg'
alt=''
onClick={groupStop}
/>
<img
src='/src/images/trash.svg'
alt=''
onClick={groupDelete}
/>
</div>
</StoreSelectNotification>
)}
<StoreTable
loadingState={loadingState}
activeColumns={activeColumns}
setActiveColumns={setActiveColumns}
Apps={Apps}
checkedRows={checkedRows}
setCheckedRows={setCheckedRows}
selectAll={selectAll}
notChecked={notChecked}
setNotChecked={setNotChecked}
setSelectAll={setSelectAll}
getApps={getApps}
queryParams={queryParams}
setQueryParams={setQueryParams}
/>
<div className='bottom-menu'>
<Pagination
queryParams={queryParams}
setQueryParams={setQueryParams}
totalCount={totalCount}
/>
<MainButton onClick={() => setModal(true)}>
Добавить
</MainButton>
</div>
</>
)}
</>
</>
)}
</StoreContainer>
)}
<NewAppModal modal={modal} setModal={setModal} getApps={getApps} />
</>
)
}

View File

@ -0,0 +1,390 @@
import { useEffect, useRef, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Textarea from 'src/components/UI/input/Textarea'
import Loader from 'src/components/UI/loader/Loader'
import Modal from 'src/components/UI/modal/Modal'
import { HOST_NAME } from 'src/constants/host'
import StoreService from 'src/services/storeServices'
import { T_App, T_NewApp } from '../type'
import {
LoaderContainer,
NewAppButtonBlock,
NewAppErrorMessage,
NewAppErrorMessageContainer,
NewAppFileBlock,
NewAppIconBlock,
NewAppModalContainer,
NewAppScreenshotsBlock
} from './newAppModal/styles'
type Props = {
modal: boolean
setModal: (param: boolean) => void
app: T_App
getApps: () => void
}
interface screenshotArr {
id: number
file: File | string
}
export default function EditAppModal({ modal, setModal, app, getApps }: Props) {
const [error, setError] = useState({
state: false,
message: ''
})
const [loading, setLoading] = useState(false)
const setErrorState = (value: boolean) => {
setError({ ...error, state: value })
}
const [editApp, setEditApp] = useState<T_NewApp>({
Name: app.Name,
Description: app.Description,
ShortDescription: app.ShortDescription,
Version: app.Version,
Category: app.Category
})
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditApp({ ...editApp, Name: e.target.value })
}
const handleDescriptionChange = (
e: React.ChangeEvent<HTMLTextAreaElement>
) => {
setEditApp({ ...editApp, Description: e.target.value })
}
const handleShortDescriptionChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setEditApp({ ...editApp, ShortDescription: e.target.value })
}
const handleVersionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditApp({ ...editApp, Version: e.target.value })
}
const handleCategoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditApp({ ...editApp, Category: e.target.value })
}
const [formValid, setFormValid] = useState(false)
useEffect(() => {
setFormValid(
editApp.Name !== '' &&
editApp.Description !== '' &&
editApp.ShortDescription !== '' &&
editApp.Version !== '' &&
editApp.Category !== ''
)
}, [editApp])
const iconInputRef = useRef<HTMLInputElement>(null)
const screenshotsInputRef = useRef<HTMLInputElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedImage, setSelectedImage] = useState<File | string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | string | null>(null)
const [selectedScreenshots, setSelectedScreenshots] = useState<
screenshotArr[]
>([])
const [deleteScreenshots, setDeleteScreenshots] = useState<string[]>([])
useEffect(() => {
setSelectedScreenshots(
app.ImageUrl.map((item, index) => ({
id: new Date().getTime() + index,
file: item
}))
)
setSelectedImage(app.IconUrl)
setSelectedFile(app.Url)
}, [modal])
// Иконка
const handleImageChange = () => {
const file = iconInputRef.current?.files?.[0]
if (file && file.type.startsWith('image/')) {
setSelectedImage(file)
}
}
const handleImageClick = () => {
iconInputRef.current?.click()
}
// Скриншоты
const handleScreenshotsChange = () => {
if (screenshotsInputRef.current?.files) {
const filesArray = Array.from(screenshotsInputRef.current.files)
const newScreenshots = filesArray.map((file, index) => ({
id: new Date().getTime() + index,
file: file
}))
setSelectedScreenshots((prevScreenshots) => [
...prevScreenshots,
...newScreenshots
])
}
if (screenshotsInputRef.current) {
screenshotsInputRef.current.value = ''
}
}
const deleteScreenshot = (id: number) => {
const screenshotToDelete: screenshotArr | undefined =
selectedScreenshots.find((screenshot) => screenshot.id === id)
if (screenshotToDelete) {
if (typeof screenshotToDelete.file === 'string') {
setDeleteScreenshots((prevState) => [
...prevState,
screenshotToDelete.file as string
])
}
setSelectedScreenshots((prevScreenshots) =>
prevScreenshots.filter((screenshot) => screenshot.id !== id)
)
}
}
const handleScreenshotsClick = () => {
screenshotsInputRef.current?.click()
}
// Файл
const handleFileChange = () => {
const file = fileInputRef.current?.files?.[0]
if (file) {
setSelectedFile(file)
}
}
const handleFileClick = () => {
fileInputRef.current?.click()
}
const updateApp = async () => {
setLoading(true)
try {
// Данные
if (
!(
editApp.Category === app.Category &&
editApp.Description === app.Description &&
editApp.Name === app.Name &&
editApp.ShortDescription === app.ShortDescription &&
editApp.Version === app.Version
)
) {
const response = await StoreService.editData(app.Id, editApp)
if (response.status === 406) {
setError({ state: true, message: response.data })
setLoading(false)
return
}
}
// Иконка
if (app.IconUrl !== selectedImage) {
const response =
selectedImage instanceof File &&
(await StoreService.addIcon(selectedImage, app.Id))
if (response && response.status === 406) {
setError({ state: true, message: response.data })
setLoading(false)
return
}
}
// Скриншоты
if (
!(
app.ImageUrl.length === selectedScreenshots.length &&
deleteScreenshots.length === 0
)
) {
await Promise.all(
deleteScreenshots.map(
async (item) => await StoreService.deleteScreenshots(app.Id, item)
)
)
const filesToAdd: File[] = selectedScreenshots
.filter((item) => item.file instanceof File)
.map((item) => item.file as File)
filesToAdd.length !== 0 &&
(await StoreService.addScreenshots(filesToAdd, app.Id))
}
// Файл
if (app.Url !== selectedFile && selectedFile instanceof File) {
const response = await StoreService.addFile(selectedFile, app.Id)
if (response.status === 406) {
setError({ state: true, message: response.data })
setLoading(false)
return
}
}
setLoading(false)
setModal(false)
getApps()
} catch (error) {}
}
return (
<>
<Modal modal={modal} setModal={setModal}>
<NewAppModalContainer>
{loading && (
<LoaderContainer>
<div className='container'></div>
<Loader />
</LoaderContainer>
)}
<h2>Программа для магазина</h2>
<div className='top-block'>
<div className='input-block'>
<div className='top-input-block'>
<MainInput
placeholder='Название программы'
value={editApp.Name}
onChange={handleNameChange}
/>
<MainInput
placeholder='Версия программы'
value={editApp.Version}
onChange={handleVersionChange}
/>
</div>
<MainInput
placeholder='Краткое описание'
value={editApp.ShortDescription}
onChange={handleShortDescriptionChange}
/>
</div>
<NewAppIconBlock>
{selectedImage ? (
<img
src={
typeof selectedImage === 'string'
? `${HOST_NAME}/${selectedImage}`
: URL.createObjectURL(selectedImage)
}
alt='Selected'
className='icon'
onClick={handleImageClick}
/>
) : (
<div onClick={handleImageClick} className='file-input'>
<img src='/src/images/Icon.svg' alt='' />
<p>
Выбрать
<br />
иконку
</p>
</div>
)}
<input
ref={iconInputRef}
type='file'
accept='image/*'
onChange={handleImageChange}
/>
</NewAppIconBlock>
</div>
<Textarea
placeholder='Полное описание'
value={editApp.Description}
onChange={handleDescriptionChange}
/>
<MainInput
placeholder='Категория'
value={editApp.Category}
onChange={handleCategoryChange}
/>
<NewAppScreenshotsBlock>
<div className='screenshots-input' onClick={handleScreenshotsClick}>
<img src='/src/images/Icon.svg' alt='' />
<p>
Выбрать
<br />
скриншот
</p>
<input
ref={screenshotsInputRef}
type='file'
accept='image/*'
multiple
onChange={handleScreenshotsChange}
/>
</div>
{selectedScreenshots &&
selectedScreenshots.map((screenshot) => (
<div className='screenshots-item'>
<div
className='close-icon'
onClick={() => deleteScreenshot(screenshot.id)}
>
<img src='/src/images/close.svg' alt='' />
</div>
<img
key={screenshot.id}
src={
typeof screenshot.file === 'string'
? `${HOST_NAME}/${screenshot.file}`
: URL.createObjectURL(screenshot.file)
}
alt={`Screenshot ${screenshot.id}`}
/>
</div>
))}
</NewAppScreenshotsBlock>
<NewAppFileBlock>
<input
ref={fileInputRef}
type='file'
accept='.exe'
onChange={handleFileChange}
/>
{selectedFile ? (
<p>
<span>Выбранный файл: </span>
{typeof selectedFile === 'string'
? selectedFile.replace('packages/', '')
: selectedFile.name}
</p>
) : (
<p>
<span>Файл не выбран</span>
</p>
)}
<MainButton onClick={handleFileClick}>Выбрать файл</MainButton>
</NewAppFileBlock>
<NewAppButtonBlock>
<MainButton
color='secondary'
fullWidth
onClick={() => setModal(false)}
>
Отмена
</MainButton>
<MainButton fullWidth onClick={updateApp} disabled={!formValid}>
Сохранить
</MainButton>
</NewAppButtonBlock>
</NewAppModalContainer>
</Modal>
<NewAppErrorMessageContainer>
<Modal modal={error.state} setModal={setErrorState}>
<NewAppErrorMessage>
<h3>{error.message}</h3>
<MainButton onClick={() => setErrorState(false)}>Ok</MainButton>
</NewAppErrorMessage>
</Modal>
</NewAppErrorMessageContainer>
</>
)
}

View File

@ -0,0 +1,225 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox'
import Table from 'src/components/UI/table/Table'
import { T_ColumnsState } from 'src/pages/Registrations/types'
import { I_QueryParams } from 'src/services/type'
import styled, { css } from 'styled-components'
import { T_App } from '../type'
import StoreTableItem from './StoreTableItem'
interface I_TableHeaderItem {
active: boolean
state: boolean
}
const Item = styled.div`
display: flex;
gap: 10px;
cursor: pointer;
`
const TableHeaderItem = styled.div<I_TableHeaderItem>`
display: flex;
align-items: center;
cursor: pointer;
img {
${(props) =>
props.state &&
css`
transform: rotate(180deg);
`}
${(props) =>
props.active
? css`
display: block;
`
: css`
display: none;
`}
}
`
type Props = {
activeColumns: T_ColumnsState
setActiveColumns: (param: any) => void
Apps: T_App[]
checkedRows: number[]
setCheckedRows: (param: any) => void
selectAll: boolean
notChecked: number[]
setNotChecked: (param: any) => void
setSelectAll: (param: any) => void
getApps: () => void
queryParams: I_QueryParams
setQueryParams: (param: I_QueryParams) => void
loadingState?: boolean
}
export default function StoreTable({
activeColumns,
setActiveColumns,
Apps,
checkedRows,
setCheckedRows,
selectAll,
notChecked,
setNotChecked,
setSelectAll,
getApps,
queryParams,
setQueryParams,
loadingState
}: Props) {
const [contextMenuState, setContextMenuState] = useState(false)
const [activeSort, setActiveSort] = useState('')
const [sortState, setSortState] = useState(false)
const navigate = useNavigate()
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
const orderParam = searchParams.get('order')
// Проверяем наличие параметра order в URL
if (orderParam) {
const sortOptions: { [key: string]: string } = {
name: 'Название',
updated: 'Дата обновления',
version: 'Версия',
short: 'Краткое описание',
enabled: 'Состояние'
}
const activeSortValue = sortOptions[orderParam.replace('!', '')]
const sortStateValue = orderParam[0] === '!'
setActiveSort(activeSortValue || '')
setSortState(sortStateValue)
}
}, [])
const sort = (value: string) => {
const sortOptions: { [key: string]: string } = {
Название: 'name',
'Дата обновления': 'updated',
Версия: 'version',
'Краткое описание': 'short',
Состояние: 'enabled'
}
const sortParam = sortOptions[value]
if (!sortParam) return
setActiveSort(value)
const newOrder =
queryParams.order === sortParam ? `!${sortParam}` : sortParam
setQueryParams({ ...queryParams, order: newOrder })
setSortState(queryParams.order === sortParam)
const searchParams = new URLSearchParams(window.location.search)
if (searchParams.has('order')) {
searchParams.delete('order')
}
if (newOrder.trim() !== '') {
searchParams.append('order', newOrder.trim())
}
navigate(`?${searchParams.toString()}`)
}
return (
<Table loaderState={loadingState}>
<thead>
<tr>
<th>
<Checkbox
value={
selectAll
? notChecked.length === 0
? 'ok'
: 'minus'
: checkedRows.length === 0
? 'off'
: 'minus'
}
onClick={() => {
selectAll
? (setSelectAll(false), setNotChecked([]), setCheckedRows([]))
: checkedRows.length === 0
? (setSelectAll(true), setNotChecked([]))
: setCheckedRows([])
}}
/>
</th>
{Object.entries(activeColumns).map(
([index, item]) =>
item.status === true && (
<th>
<TableHeaderItem
key={index}
active={activeSort === item.name}
state={sortState}
onClick={() => sort(item.name)}
>
<img src='/src/images/arrow-for-table.svg' alt='' />
{item.name}
</TableHeaderItem>
</th>
)
)}
<th>
<ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
positionTop
>
<div className='target'>
<img
src='/src/images/params.svg'
alt=''
onClick={() => setContextMenuState(!contextMenuState)}
/>
</div>
<div className='content'>
{Object.entries(activeColumns).map(([key, item]) => (
<Item
key={key}
onClick={() => {
const updatedActiveColumns = { ...activeColumns }
updatedActiveColumns[key].status =
!updatedActiveColumns[key].status
setActiveColumns(updatedActiveColumns)
}}
>
<Checkbox
value={item.status ? 'ok' : 'off'}
onClick={() => {}}
/>
<p>{item.name}</p>
</Item>
))}
</div>
</ContextMenu>
</th>
</tr>
</thead>
<tbody>
{Apps.map((app) => (
<StoreTableItem
app={app}
selectAll={selectAll}
notChecked={notChecked}
checkedRows={checkedRows}
setNotChecked={setNotChecked}
setCheckedRows={setCheckedRows}
activeColumns={activeColumns}
getApps={getApps}
/>
))}
</tbody>
</Table>
)
}

View File

@ -0,0 +1,212 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import MainButton from 'src/components/UI/button/MainButton'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox'
import Modal from 'src/components/UI/modal/Modal'
import { HOST_NAME } from 'src/constants/host'
import { T_ColumnsState } from 'src/pages/Registrations/types'
import StoreService from 'src/services/storeServices'
import styled from 'styled-components'
import { T_App } from '../type'
import EditAppModal from './EditAppModal'
type Props = {
app: T_App
selectAll: boolean
notChecked: number[]
checkedRows: number[]
setNotChecked: (param: any) => void
setCheckedRows: (param: any) => void
activeColumns: T_ColumnsState
getApps: () => void
}
const Item = styled.div`
display: flex;
gap: 10px;
cursor: pointer;
img {
/* width: 20px; */
}
`
const DeleteNotification = styled.div`
.button-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
`
const Icon = styled.img`
width: 80px;
`
export default function StoreTableItem({
app,
selectAll,
notChecked,
checkedRows,
setNotChecked,
setCheckedRows,
activeColumns,
getApps
}: Props) {
const [contextMenuState, setContextMenuState] = useState(false)
const [deleteNotificationState, setDeleteNotificationState] = useState(false)
const [editModalState, setEditModalState] = useState(false)
const interaction = async (type: string) => {
setContextMenuState(false)
try {
await StoreService.interaction(type, app.Id)
getApps()
} catch (error) {
console.log(error)
}
}
const deleteApp = async () => {
setDeleteNotificationState(false)
await StoreService.delete(app.Id)
getApps()
}
return (
<tr key={app.Id}>
<td>
<Checkbox
value={
selectAll
? notChecked.includes(app.Id)
? 'off'
: 'ok'
: checkedRows.includes(app.Id)
? 'ok'
: 'off'
}
onClick={() => {
selectAll
? notChecked.includes(app.Id)
? setNotChecked(notChecked.filter((item) => item !== app.Id))
: setNotChecked([...notChecked, app.Id])
: checkedRows.includes(app.Id)
? (setCheckedRows(checkedRows.filter((item) => item !== app.Id)),
setNotChecked([...notChecked, app.Id]))
: setCheckedRows([...checkedRows, app.Id])
}}
/>
</td>
{Object.entries(activeColumns).map(([key, _]) =>
activeColumns[key as keyof typeof activeColumns].status ? (
<td key={key}>
<p>
{' '}
{key === 'Enabled' ? (
app[key as keyof typeof app] ? (
<img src='/src/images/on.svg' />
) : (
<img src='/src/images/off.svg' />
)
) : key === 'IconUrl' ? (
app[key as keyof typeof app] === '' ? (
<p>Иконки нет</p>
) : (
<Icon
src={`${HOST_NAME}/${app[key as keyof typeof app]}`}
alt=''
/>
)
) : (
app[key as keyof typeof app]
)}
</p>
</td>
) : null
)}
<td>
<ContextMenu active={contextMenuState} setActive={setContextMenuState}>
<div className='target'>
<img
src='/src/images/menu.svg'
alt=''
onClick={() => setContextMenuState(!contextMenuState)}
/>
</div>
<div className='content'>
{app.Enabled ? (
<Item onClick={() => interaction('disable')}>
<img src='/src/images/stop.svg' alt='' />
<p>Выключить</p>
</Item>
) : (
<Item onClick={() => interaction('enable')}>
<img src='/src/images/play.svg' alt='' />
<p>Включить</p>
</Item>
)}
<Item onClick={() => setDeleteNotificationState(true)}>
<img src='/src/images/trash.svg' alt='' />
<p>Удалить</p>
</Item>
{app.Url !== '' && (
<Link
to={`${HOST_NAME}/${app.Url}`}
onClick={() => setContextMenuState(false)}
>
<Item>
<img
src='/src/images/download.svg'
alt=''
style={{ width: '20px' }}
/>
<p>Скачать</p>
</Item>
</Link>
)}
<Item onClick={() => interaction('repack')}>
<img src='/src/images/reload.svg' alt='' />
<p>Перепаковать</p>
</Item>
<Item
onClick={() => (
setEditModalState(true), setContextMenuState(false)
)}
>
<img src='/src/images/edit.svg' alt='' />
<p>Редактировать</p>
</Item>
</div>
</ContextMenu>
</td>
<Modal
modal={deleteNotificationState}
setModal={setDeleteNotificationState}
>
<DeleteNotification>
<h3>Вы действительно хотите удалить приложение?</h3>
<div className='button-container'>
<MainButton
color={'secondary'}
fullWidth={true}
onClick={() => setDeleteNotificationState(false)}
>
Отмена
</MainButton>
<MainButton fullWidth={true} onClick={() => deleteApp()}>
Удалить
</MainButton>
</div>
</DeleteNotification>
</Modal>
<EditAppModal
modal={editModalState}
setModal={setEditModalState}
app={app}
getApps={getApps}
/>
</tr>
)
}

View File

@ -0,0 +1,307 @@
import { useEffect, useRef, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput'
import Textarea from 'src/components/UI/input/Textarea'
import Modal from 'src/components/UI/modal/Modal'
import StoreService from 'src/services/storeServices'
import { T_NewApp } from '../../type'
import {
NewAppButtonBlock,
NewAppErrorMessage,
NewAppErrorMessageContainer,
NewAppFileBlock,
NewAppIconBlock,
NewAppModalContainer,
NewAppScreenshotsBlock
} from './styles'
interface screenshotArr {
id: number
file: File
}
type Props = {
modal: boolean
setModal: (param: boolean) => void
getApps: () => void
}
export default function NewAppModal({ modal, setModal, getApps }: Props) {
const [newApp, setNewApp] = useState<T_NewApp>({
Name: '',
Description: '',
ShortDescription: '',
Version: '',
Category: ''
})
const [selectedImage, setSelectedImage] = useState<File | null>(null)
const [selectedScreenshots, setSelectedScreenshots] = useState<
screenshotArr[]
>([])
const [selectedFile, setSelectedFile] = useState<File | null>(null)
useEffect(() => {
setNewApp({
Name: '',
Description: '',
ShortDescription: '',
Version: '',
Category: ''
})
setSelectedImage(null)
setSelectedScreenshots([])
setSelectedFile(null)
}, [modal])
const [formValid, setFormValid] = useState(false)
useEffect(() => {
setFormValid(
newApp.Name !== '' &&
newApp.Description !== '' &&
newApp.ShortDescription !== '' &&
newApp.Version !== '' &&
newApp.Category !== ''
)
}, [newApp])
const [errorState, setErrorState] = useState(false)
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewApp({ ...newApp, Name: e.target.value })
}
const handleDescriptionChange = (
e: React.ChangeEvent<HTMLTextAreaElement>
) => {
setNewApp({ ...newApp, Description: e.target.value })
}
const handleShortDescriptionChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setNewApp({ ...newApp, ShortDescription: e.target.value })
}
const handleVersionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewApp({ ...newApp, Version: e.target.value })
}
const handleCategoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewApp({ ...newApp, Category: e.target.value })
}
const iconInputRef = useRef<HTMLInputElement>(null)
const handleImageChange = () => {
const file = iconInputRef.current?.files?.[0]
if (file && file.type.startsWith('image/')) {
setSelectedImage(file)
}
}
const handleImageClick = () => {
iconInputRef.current?.click()
}
const screenshotsInputRef = useRef<HTMLInputElement>(null)
const handleScreenshotsChange = () => {
if (screenshotsInputRef.current?.files) {
const filesArray = Array.from(screenshotsInputRef.current.files)
const newScreenshots = filesArray.map((file, index) => ({
id: new Date().getTime() + index,
file: file
}))
setSelectedScreenshots((prevScreenshots) => [
...prevScreenshots,
...newScreenshots
])
}
if (screenshotsInputRef.current) {
screenshotsInputRef.current.value = ''
}
}
const deleteScreenshot = (id: number) => {
setSelectedScreenshots((prevScreenshots) =>
prevScreenshots.filter((screenshot) => screenshot.id !== id)
)
}
const handleScreenshotsClick = () => {
screenshotsInputRef.current?.click()
}
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = () => {
const file = fileInputRef.current?.files?.[0]
if (file) {
setSelectedFile(file)
}
}
const handleFileClick = () => {
fileInputRef.current?.click()
}
const appCreate = async () => {
try {
const response = await StoreService.createApp(newApp)
if (response.status === 200) {
selectedScreenshots !== null &&
(await StoreService.addScreenshots(
selectedScreenshots.map((item) => item.file),
response.data.Id
))
selectedImage !== null &&
(await StoreService.addIcon(selectedImage, response.data.Id))
selectedFile !== null &&
(await StoreService.addFile(selectedFile, response.data.Id))
} else if (response.status === 406) {
setErrorState(true)
}
setModal(false)
getApps()
} catch (error) {
console.log(error)
}
}
return (
<>
<Modal modal={modal} setModal={setModal}>
<NewAppModalContainer>
<h2>Программа для магазина</h2>
<div className='top-block'>
<div className='input-block'>
<div className='top-input-block'>
<MainInput
placeholder='Название программы'
value={newApp.Name}
onChange={handleNameChange}
/>
<MainInput
placeholder='Версия программы'
value={newApp.Version}
onChange={handleVersionChange}
/>
</div>
<MainInput
placeholder='Краткое описание'
value={newApp.ShortDescription}
onChange={handleShortDescriptionChange}
/>
</div>
<NewAppIconBlock>
{selectedImage ? (
<img
src={URL.createObjectURL(selectedImage)}
alt='Selected'
className='icon'
onClick={handleImageClick}
/>
) : (
<div onClick={handleImageClick} className='file-input'>
<img src='/src/images/Icon.svg' alt='' />
<p>
Выбрать
<br />
иконку
</p>
</div>
)}
<input
ref={iconInputRef}
type='file'
accept='image/*'
onChange={handleImageChange}
/>
</NewAppIconBlock>
</div>
<Textarea
placeholder='Полное описание'
value={newApp.Description}
onChange={handleDescriptionChange}
/>
<MainInput
placeholder='Категория'
value={newApp.Category}
onChange={handleCategoryChange}
/>
<NewAppScreenshotsBlock>
<div className='screenshots-input' onClick={handleScreenshotsClick}>
<img src='/src/images/Icon.svg' alt='' />
<p>
Выбрать
<br />
скриншот
</p>
<input
ref={screenshotsInputRef}
type='file'
accept='image/*'
multiple
onChange={handleScreenshotsChange}
/>
</div>
{selectedScreenshots &&
selectedScreenshots.map((screenshot) => (
<div className='screenshots-item'>
<div
className='close-icon'
onClick={() => deleteScreenshot(screenshot.id)}
>
<img src='/src/images/close.svg' alt='' />
</div>
<img
key={screenshot.id}
src={URL.createObjectURL(screenshot.file)}
alt={`Screenshot ${screenshot.id}`}
/>
</div>
))}
</NewAppScreenshotsBlock>
<NewAppFileBlock>
<input
ref={fileInputRef}
type='file'
accept='.exe'
onChange={handleFileChange}
/>
{selectedFile ? (
<p>
<span>Выбранный файл: </span>
{selectedFile.name}
</p>
) : (
<p>
<span>Файл не выбран</span>
</p>
)}
<MainButton onClick={handleFileClick}>Выбрать файл</MainButton>
</NewAppFileBlock>
<NewAppButtonBlock>
<MainButton
color='secondary'
fullWidth
onClick={() => setModal(false)}
>
Отмена
</MainButton>
<MainButton fullWidth onClick={appCreate} disabled={!formValid}>
Создать
</MainButton>
</NewAppButtonBlock>
</NewAppModalContainer>
</Modal>
<NewAppErrorMessageContainer>
<Modal modal={errorState} setModal={setErrorState}>
<NewAppErrorMessage>
<h3>Имя и версия уже существует</h3>
<MainButton onClick={() => setErrorState(false)}>Ok</MainButton>
</NewAppErrorMessage>
</Modal>
</NewAppErrorMessageContainer>
</>
)
}

View File

@ -0,0 +1,190 @@
import styled from 'styled-components'
export const NewAppModalContainer = styled.div`
position: relative;
width: 826px;
display: flex;
flex-direction: column;
gap: 20px;
.top-block {
display: flex;
gap: 20px;
}
.input-block {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
.top-input-block {
display: flex;
gap: 20px;
width: 100%;
}
`
export const NewAppIconBlock = styled.div`
cursor: pointer;
height: 125px;
width: 125px;
.icon {
width: 125px;
height: 125px;
object-fit: cover;
border-radius: 12px;
}
input {
display: none;
}
.file-input {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 10px;
border-radius: 12px;
border: 2px dashed ${(props) => props.theme.textColor};
height: 125px;
width: 125px;
opacity: 0.6;
}
`
export const NewAppScreenshotsBlock = styled.div`
display: flex;
gap: 20px;
width: 826px;
padding-bottom: 10px;
overflow-x: auto;
&::-webkit-scrollbar {
height: 5px; /* Ширина скролбара */
}
&::-webkit-scrollbar-track {
background: #e3e3e3; /* Цвет фона трека скролбара */
}
&::-webkit-scrollbar-thumb {
background: #b0b0b0; /* Цвет ползунка */
border-radius: 12px;
}
.screenshots-input {
gap: 10px;
border-radius: 12px;
border: 2px dashed ${(props) => props.theme.textColor};
height: 130px;
min-width: 230px;
opacity: 0.6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
}
input {
display: none;
}
.screenshots-item {
${(props) => props.theme.mainShadow}
position: relative;
height: 130px;
min-width: 230px;
border-radius: 12px;
overflow: hidden;
img {
object-fit: cover;
height: 130px;
width: 230px;
}
&:hover {
.close-icon {
display: block;
}
}
.close-icon {
position: absolute;
right: 10px;
top: 10px;
background: ${(props) => props.theme.bg};
width: 25px;
height: 25px;
border-radius: 20px;
cursor: pointer;
display: none;
img {
width: 25px;
height: 25px;
}
}
}
`
export const NewAppFileBlock = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
input {
display: none;
}
p {
font-size: 18px;
}
span {
font-weight: 600;
}
`
export const NewAppButtonBlock = styled.div`
display: flex;
gap: 20px;
`
export const NewAppErrorMessageContainer = styled.div`
position: relative;
z-index: 4;
`
export const NewAppErrorMessage = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 20px;
`
export const LoaderContainer = styled.div`
position: absolute;
top: 0;
left: 0;
z-index: 2;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.container {
position: absolute;
background: ${(props) => props.theme.bg};
width: 101%;
height: 101%;
opacity: 0.5;
}
`

68
src/pages/Store/styles.ts Normal file
View File

@ -0,0 +1,68 @@
import styled from 'styled-components'
export const StoreLoaderContainer = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const StoreContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
align-items: start;
padding: 40px 20px;
.bottom-menu {
display: flex;
align-items: start;
width: 100%;
justify-content: space-between;
}
`
export const StoreNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
`
export const SearchNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const StoreSelectNotification = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
${(props) => props.theme.mainShadow}
border-radius: 12px;
.menu {
display: flex;
gap: 10px;
}
img {
padding: 5px;
cursor: pointer;
border-radius: 6px;
transition: 300ms;
&:hover {
background: ${(props) => props.theme.focusColor};
transform: scale(1.1);
}
}
`

25
src/pages/Store/type.ts Normal file
View File

@ -0,0 +1,25 @@
export type T_NewApp = {
Name: string
Description: string
ShortDescription: string
Version: string
Category: string
}
export interface T_App {
Id: number;
Name: string;
Description: string;
ShortDescription: string;
IconUrl: string;
ImageUrl: string[];
Version: string;
DependenciesReceived: boolean;
Arch: "amd64" | "x86" | string;
Category: string;
CreatedDate: string;
UpdatedDate: string;
Repo: boolean;
Url: string;
Enabled: boolean;
}

View File

@ -0,0 +1,89 @@
import axios, { AxiosResponse } from 'axios'
import { API_HOST_NAME } from 'src/constants/host'
import { T_NewRegistration } from 'src/pages/Registrations/types'
import { I_QueryParams } from './type'
export default class RegistrationsService {
static async getRegistrations(
queryParams: I_QueryParams
): Promise<AxiosResponse> {
const { page, count, search, order } = queryParams
const url = `${API_HOST_NAME}/regs`
const params = new URLSearchParams()
if (page !== undefined) params.append('page', String(page))
if (count !== undefined) params.append('count', String(count))
if (search !== undefined && search !== '') params.append('search', search)
if (order !== undefined) params.append('order', order)
try {
const response = await axios.get(url, { params })
console.log(response.data)
return response
} catch (error: any) {
return error.response
}
}
static async createRegistration(
newReg: T_NewRegistration
): Promise<AxiosResponse> {
const date = new Date(newReg.EndDate)
const isoDateStr = date.toISOString()
newReg.EndDate = isoDateStr
if (typeof newReg.Count === 'string') {
newReg.Count = parseInt(newReg.Count, 10)
}
try {
const response = await axios.post(`${API_HOST_NAME}/regs`, newReg)
return response
} catch (error: any) {
return error.response
}
}
static async deleteRegistration(id: number): Promise<AxiosResponse> {
try {
const response = await axios.delete(`${API_HOST_NAME}/regs/${id}`)
return response
} catch (error: any) {
return error.response
}
}
static async onRegistration(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/regs/${id}/state/enable`)
return response
} catch (error: any) {
return error.response
}
}
static async offRegistration(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/regs/${id}/state/disable`)
return response
} catch (error: any) {
return error.response
}
}
static async editRegistration(
id: number,
reg: T_NewRegistration
): Promise<AxiosResponse> {
const date = new Date(reg.EndDate)
const isoDateStr = date.toISOString()
reg.EndDate = isoDateStr
if (typeof reg.Count === 'string') {
reg.Count = parseInt(reg.Count, 10)
}
try {
const response = await axios.post(`${API_HOST_NAME}/regs/${id}`, reg)
return response
} catch (error: any) {
return error.response
}
}
}

View File

@ -0,0 +1,119 @@
import axios, { AxiosResponse } from 'axios'
import { API_HOST_NAME } from 'src/constants/host'
import { T_NewRepository } from 'src/pages/Repository/types'
import { I_QueryParams } from './type'
export default class RepositoryService {
static async getRepository(
queryParams: I_QueryParams
): Promise<AxiosResponse> {
const { page, count, search, order } = queryParams
const url = `${API_HOST_NAME}/repository/packages?full=true`
const params = new URLSearchParams()
if (page !== undefined) params.append('page', String(page))
if (count !== undefined) params.append('count', String(count))
if (search !== undefined && search !== '') params.append('search', search)
if (order !== undefined) params.append('order', order)
try {
const response = await axios.get(url, { params })
console.log(response.data)
return response
} catch (error: any) {
return error.response
}
}
static async createRep(
newRep: T_NewRepository
): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/repository/packages`, newRep)
return response
} catch (error: any) {
return error.response
}
}
static async addFile(file: File, id: number): Promise<AxiosResponse> {
try {
const formData = new FormData()
formData.append(`file`, file)
const response = await axios.post(
`${API_HOST_NAME}/repository/packages/${id}/package/upload`,
formData
)
return response
} catch (error: any) {
return error.response
}
}
static async onRepository(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/repository/packages/${id}/enable`)
return response
} catch (error: any) {
return error.response
}
}
static async offRepository(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/repository/packages/${id}/disable`)
return response
} catch (error: any) {
return error.response
}
}
static async RepackReposytory(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/repository/packages/${id}/repack`)
return response
} catch (error: any) {
return error.response
}
}
static async DependenciesReposytory(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/repository/packages/${id}/dependencies`)
return response
} catch (error: any) {
return error.response
}
}
static async DownloadFile(id: number): Promise<AxiosResponse> {
try {
const response = await axios.get(
`${API_HOST_NAME}/repository/packages/${id}/package/download`,
{
responseType: 'arraybuffer',
}
)
return response;
} catch (error: any) {
return error.response;
}
}
static async deleteRepository(id: number): Promise<AxiosResponse> {
try {
const response = await axios.delete(`${API_HOST_NAME}/repository/packages/${id}`)
return response
} catch (error: any) {
return error.response
}
}
static async editRepository(
id: number,
rep: T_NewRepository
): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/repository/packages/${id}`, rep)
return response
} catch (error: any) {
return error.response
}
}
}

View File

@ -0,0 +1,120 @@
import axios, { AxiosResponse } from 'axios'
import { API_HOST_NAME } from 'src/constants/host'
import { T_NewApp } from 'src/pages/Store/type'
import { I_QueryParams } from './type'
export default class StoreService {
static async getApp(
queryParams: I_QueryParams
): Promise<AxiosResponse> {
const { page, count, search, order } = queryParams
const url = `${API_HOST_NAME}/packages?full=true`
const params = new URLSearchParams()
if (page !== undefined) params.append('page', String(page))
if (count !== undefined) params.append('count', String(count))
if (search !== undefined && search !== '') params.append('search', search)
if (order !== undefined) params.append('order', order)
try {
const response = await axios.get(url, { params })
console.log(response.data)
return response
} catch (error: any) {
return error.response
}
}
static async createApp(newApp: T_NewApp): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/packages`, newApp)
return response
} catch (error: any) {
return error.response
}
}
static async addScreenshots(
screenshots: File[],
id: number
): Promise<AxiosResponse> {
try {
const formData = new FormData()
screenshots.forEach((screenshot) => {
formData.append(`files`, screenshot)
})
const response = await axios.post(
`${API_HOST_NAME}/packages/${id}/images/upload`,
formData
)
return response
} catch (error: any) {
return error.response
}
}
static async addIcon(icon: File, id: number): Promise<AxiosResponse> {
try {
const formData = new FormData()
formData.append(`file`, icon)
const response = await axios.post(
`${API_HOST_NAME}/packages/${id}/icon/upload`,
formData
)
return response
} catch (error: any) {
return error.response
}
}
static async addFile(file: File, id: number): Promise<AxiosResponse> {
try {
const formData = new FormData()
formData.append(`file`, file)
const response = await axios.post(
`${API_HOST_NAME}/packages/${id}/package/upload`,
formData
)
return response
} catch (error: any) {
return error.response
}
}
static async interaction(type: string, id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/packages/${id}/${type}`)
return response
} catch (error: any) {
return error.response
}
}
static async deleteScreenshots(id: number, name: string): Promise<AxiosResponse> {
try {
const response = await axios.delete(`${API_HOST_NAME}/packages/${id}/image/${name.replace('images/', '')}`)
return response
} catch (error: any) {
return error.response
}
}
static async editData(id: number, editApp: T_NewApp): Promise<AxiosResponse> {
try {
const response = await axios.post(`${API_HOST_NAME}/packages/${id}`, editApp)
return response
} catch (error: any) {
return error.response
}
}
static async delete(id: number): Promise<AxiosResponse> {
try {
const response = await axios.delete(`${API_HOST_NAME}/packages/${id}`)
return response
} catch (error: any) {
return error.response
}
}
}

6
src/services/type.ts Normal file
View File

@ -0,0 +1,6 @@
export interface I_QueryParams {
page: number
count: number
search: string
order?: string | undefined
}

View File

@ -0,0 +1,17 @@
import axios, { AxiosResponse } from 'axios'
import { API_HOST_NAME } from 'src/constants/host'
//
export default class UserService {
static async login(name: string, password: string): Promise<AxiosResponse> {
try {
const response = await axios.get(
`${API_HOST_NAME}/auth?a123=${name}&a321=${password}`
);
return response
} catch (error: any) {
return error.response
}
}
}

View File

@ -18,7 +18,12 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"src/*": ["src/*"]
}
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]

View File

@ -1,7 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import dotenv from 'dotenv'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react(), VitePWA()],
resolve: {
alias: {
src: '/src'
}
},
define: {
'process.env': dotenv.config().parsed
}
}) })