add store page

This commit is contained in:
иосиф брыков 2024-02-22 19:48:03 +05:00
parent c655868fe0
commit 8ba456c243
28 changed files with 1895 additions and 63 deletions

View File

@ -2,7 +2,7 @@ import { Route, Routes } from 'react-router-dom'
import Login from './pages/Login'
import Registrations from './pages/Registrations/Registrations'
import Repository from './pages/Repository'
import Store from './pages/Store'
import Store from './pages/Store/Store'
export default function App() {
return (

View File

@ -5,13 +5,15 @@ type Props = {
active: boolean
children: React.ReactNode
setActive: (param: boolean) => void
positionTop?: boolean
}
interface I_Active {
interface I_Container {
active: boolean
positionTop?: boolean
}
const Container = styled.div<I_Active>`
const Container = styled.div<I_Container>`
position: relative;
.target {
@ -21,8 +23,10 @@ const Container = styled.div<I_Active>`
.content {
z-index: 1;
position: absolute;
top: 30px;
right: 0;
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}
@ -42,7 +46,7 @@ const Container = styled.div<I_Active>`
}
`
export default function ContextMenu({ active, children, setActive }: Props) {
export default function ContextMenu({ active, children, setActive, positionTop }: Props) {
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
@ -59,7 +63,7 @@ export default function ContextMenu({ active, children, setActive }: Props) {
}, [])
return (
<Container active={active} ref={menuRef}>
<Container active={active} ref={menuRef} positionTop={positionTop}>
{children}
</Container>
)

View File

@ -3,6 +3,7 @@ import styled from 'styled-components'
const InputContainer = styled.div`
position: relative;
width: 100%;
`
const Input = styled.input`
@ -13,7 +14,6 @@ const Input = styled.input`
font-size: 18px;
font-weight: 500;
outline: none;
width: 100%;
`

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

@ -1,16 +1,49 @@
import React from 'react'
import styled from 'styled-components'
import styled, { css } from 'styled-components'
import Loader from '../loader/Loader'
type Props = {
loaderState?: boolean
children: React.ReactNode
}
const TableContainer = styled.table`
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;
@ -29,9 +62,13 @@ const TableContainer = styled.table`
}
`
export default function Table({ children }: Props) {
export default function Table({ children, loaderState }: Props) {
return (
<TableContainer>
<TableContainer loading={loaderState}>
<div className='loader'></div>
<div className='loader-container'>
<Loader />
</div>
<table>{children}</table>
</TableContainer>
)

View File

@ -20,15 +20,6 @@ export default function Layout({ children }: Props) {
const currentPath = location.pathname
const navigate = useNavigate()
useEffect(() => {
const testQuery = async () => {
const response = await axios.get(`${HOST_NAME}/packages`)
console.log(response.data);
}
testQuery()
}, [])
// авторизация
// useEffect(() => {
// const hasCookie = document.cookie.includes('_identid')

View File

@ -1 +1,2 @@
export const HOST_NAME = 'http://localhost:8070/api/v1'
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

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

View File

@ -39,12 +39,17 @@ export default function Registrations({}: Props) {
const getReg = async () => {
setSearchNotification(false)
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))
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)
}
@ -69,8 +74,10 @@ export default function Registrations({}: Props) {
const [notChecked, setNotChecked] = useState<number[]>([])
useEffect(() => {
notChecked.length === registrations.length &&
!errorState && (
notChecked.length === registrations.length &&
(setSelectAll(false), setNotChecked([]))
)
}, [notChecked])
const groupDelete = async () => {
@ -93,11 +100,11 @@ export default function Registrations({}: Props) {
const groupStop = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.offRegistration(item)
)
RegistrationsService.offRegistration(item)
)
await Promise.all(deletePromises)
getReg()
await Promise.all(deletePromises)
getReg()
}
const [newReg, setNewReg] = useState<T_NewRegistration>({
@ -110,10 +117,6 @@ export default function Registrations({}: Props) {
const [modal, setModal] = useState(false)
useEffect(() => {
console.log(registrations)
}, [registrations])
useEffect(() => {
setNewReg({
RegNum: '',
@ -133,6 +136,8 @@ export default function Registrations({}: Props) {
{loadingState ? (
<RegLoaderContainer>
<Loader />
</RegLoaderContainer>) : errorState ? (<RegLoaderContainer>
<h2>Сервер недоступен</h2>
</RegLoaderContainer>
) : (
<RegContainer>

View File

@ -18,14 +18,14 @@ const ModalContainer = styled.div`
}
`
const ErrorMessageContainer = styled.div`
const ErrorMessageContent = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 20px;
`
const Test = styled.div`
const ErrorMessageContainer = styled.div`
z-index: 3;
`
@ -126,14 +126,14 @@ export default function NewRegModal({
</div>
</ModalContainer>
</Modal>
<Test>
<ErrorMessageContainer>
<Modal modal={errorMessage} setModal={setErrorMessage}>
<ErrorMessageContainer>
<ErrorMessageContent>
<h3>Номер контракта или поставки уже существует</h3>
<MainButton onClick={() => setErrorMessage(false)}>Ok</MainButton>
</ErrorMessageContainer>
</ErrorMessageContent>
</Modal>
</Test>
</ErrorMessageContainer>
</>
)
}

View File

@ -140,6 +140,7 @@ export default function RegTable({
<ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
positionTop
>
<div className='target'>
<img

View File

@ -33,7 +33,7 @@ const DeleteNotification = styled.div`
}
`
export default function ({
export default function RrgTableItem ({
registration,
selectAll,
notChecked,

View File

@ -1,9 +0,0 @@
import React from 'react'
type Props = {}
export default function Store({}: Props) {
return (
<div>Store</div>
)
}

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

@ -0,0 +1,215 @@
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 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 handleSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQueryParams({ ...queryParams, search: e.target.value })
}
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(() => {
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>Добавить приложение</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,195 @@
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 { T_App } from '../type'
import styled, { css } from 'styled-components'
import { I_QueryParams } from 'src/services/type'
import { useState } from 'react'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import App from 'src/App'
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 sort = (value: string) => {
const sortOptions: { [key: string]: string } = {
'Название': 'name',
'Дата обновления': 'updated',
'Версия': 'version',
// возможен косяк с short
'Краткое описание': '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);
};
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,208 @@
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/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, item]) =>
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,395 @@
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)
}, [])
// Иконка
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()
}
useEffect(() => {
console.log(selectedScreenshots)
console.log(deleteScreenshots)
}, [selectedScreenshots])
// Файл
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,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

@ -1,5 +1,5 @@
import axios, { AxiosResponse } from 'axios'
import { HOST_NAME } from 'src/constants/host'
import { API_HOST_NAME } from 'src/constants/host'
import { T_NewRegistration } from 'src/pages/Registrations/types'
import { I_QueryParams } from './type'
@ -8,7 +8,7 @@ export default class RegistrationsService {
queryParams: I_QueryParams
): Promise<AxiosResponse> {
const { page, count, search, order } = queryParams
const url = `${HOST_NAME}/regs`
const url = `${API_HOST_NAME}/regs`
const params = new URLSearchParams()
if (page !== undefined) params.append('page', String(page))
@ -35,7 +35,7 @@ export default class RegistrationsService {
newReg.Count = parseInt(newReg.Count, 10)
}
try {
const response = await axios.post(`${HOST_NAME}/regs`, newReg)
const response = await axios.post(`${API_HOST_NAME}/regs`, newReg)
return response
} catch (error: any) {
return error.response
@ -44,7 +44,7 @@ export default class RegistrationsService {
static async deleteRegistration(id: number): Promise<AxiosResponse> {
try {
const response = await axios.delete(`${HOST_NAME}/regs/${id}`)
const response = await axios.delete(`${API_HOST_NAME}/regs/${id}`)
return response
} catch (error: any) {
return error.response
@ -53,7 +53,7 @@ export default class RegistrationsService {
static async onRegistration(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${HOST_NAME}/regs/${id}/state/enable`)
const response = await axios.post(`${API_HOST_NAME}/regs/${id}/state/enable`)
return response
} catch (error: any) {
return error.response
@ -62,7 +62,7 @@ export default class RegistrationsService {
static async offRegistration(id: number): Promise<AxiosResponse> {
try {
const response = await axios.post(`${HOST_NAME}/regs/${id}/state/disable`)
const response = await axios.post(`${API_HOST_NAME}/regs/${id}/state/disable`)
return response
} catch (error: any) {
return error.response
@ -80,7 +80,7 @@ export default class RegistrationsService {
reg.Count = parseInt(reg.Count, 10)
}
try {
const response = await axios.post(`${HOST_NAME}/regs/${id}`, reg)
const response = await axios.post(`${API_HOST_NAME}/regs/${id}`, reg)
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
}
}
}

View File

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