Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
commit
7d07651323
@ -3,6 +3,7 @@ import { HeaderUi } from "../widgets/loyaout/ui/HeaderUi";
|
||||
import { FooterUi } from "../widgets/loyaout/ui/FooterUi";
|
||||
import { AuctionPage } from "../pages/AuctionPage";
|
||||
import { AuthPage } from "../pages/AuthPage";
|
||||
import { PrivateRoute } from "./PrivateRoute";
|
||||
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
@ -20,7 +21,9 @@ function App() {
|
||||
<div>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/auction" element={<AuctionPage />} />
|
||||
<Route path='/auction' element={<PrivateRoute/>}>
|
||||
<Route path='/auction' element={<AuctionPage/>}/>
|
||||
</Route>
|
||||
<Route path="/auth" element={<AuthPage />} />
|
||||
<Route path="*" element={<Navigate to="/auth" replace />} />
|
||||
</Routes>
|
||||
|
7
src/app/PrivateRoute.tsx
Normal file
7
src/app/PrivateRoute.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { getCookie } from "typescript-cookie";
|
||||
|
||||
export const PrivateRoute = () => {
|
||||
return getCookie('authToken') ? <Outlet /> : <Navigate to="/auth" />;
|
||||
}
|
18
src/entities/Ui/SearchUi/SearchUi.tsx
Normal file
18
src/entities/Ui/SearchUi/SearchUi.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import loupe from "../../../shared/images/loupe.png";
|
||||
|
||||
import styles from "./searchUi.module.scss";
|
||||
|
||||
export const SearchUi = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Найти аукцион"
|
||||
/>
|
||||
<img src={loupe} alt='loupe' />
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/entities/Ui/SearchUi/index.ts
Normal file
1
src/entities/Ui/SearchUi/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { SearchUi } from "./SearchUi"
|
25
src/entities/Ui/SearchUi/searchUi.module.scss
Normal file
25
src/entities/Ui/SearchUi/searchUi.module.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid gray;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
border: none;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
95
src/features/auth/ui/AuthLoginFormUi/AuthLoginFormUi.tsx
Normal file
95
src/features/auth/ui/AuthLoginFormUi/AuthLoginFormUi.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { ButtonUi, ButtonUiType } from "../../../../shared/UI/ButtonUi";
|
||||
|
||||
import Form from 'react-bootstrap/Form';
|
||||
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { AuthResponsePayload } from '../../../../types';
|
||||
import { api } from "../../../../query/query";
|
||||
import { setCookie } from "typescript-cookie";
|
||||
import styles from "./authLoginForm.module.scss";
|
||||
|
||||
export const AuthLoginFormUi = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const schema = yup.object().shape({
|
||||
username: yup.string()
|
||||
.min(3, 'Минимум 3 символа!')
|
||||
.max(16, 'Максимум 16 символов!')
|
||||
.required('Это поле обязательное.'),
|
||||
|
||||
password: yup.string().required('Это обязательное поле.').min(6, 'Минимум 6 символов!'),
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
errors,
|
||||
values,
|
||||
setFieldError
|
||||
} = useFormik({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
validationSchema: schema,
|
||||
onSubmit: async (values): Promise<AuthResponsePayload> => {
|
||||
return await api.post('/authorization', {
|
||||
username: values.username,
|
||||
password: values.password
|
||||
}).then((res) => {
|
||||
if (res.data[0]) {
|
||||
setFieldError('username', res.data[0])
|
||||
setFieldError('password', res.data[0])
|
||||
} else {
|
||||
setCookie('authToken', res.data!.authToken)
|
||||
setCookie('refreshToken', res.data!.refreshToken)
|
||||
navigate("/auction")
|
||||
}
|
||||
return res.data as AuthResponsePayload
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Form noValidate className={styles.container} onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="username"
|
||||
placeholder="username"
|
||||
onChange={handleChange}
|
||||
name="username"
|
||||
onBlur={handleBlur}
|
||||
value={values.username}
|
||||
isInvalid={!!errors.username}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{errors.username}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
type="password"
|
||||
id="password"
|
||||
placeholder="password"
|
||||
onChange={handleChange}
|
||||
value={values.password}
|
||||
name="password"
|
||||
onBlur={handleBlur}
|
||||
isInvalid={!!errors.password}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{errors.password}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<ButtonUi title={'Войти'} variant={ButtonUiType.PRIMARY} type={'submit'}/>
|
||||
</Form >
|
||||
)
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
.container {
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
1
src/features/auth/ui/AuthLoginFormUi/index.ts
Normal file
1
src/features/auth/ui/AuthLoginFormUi/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AuthLoginFormUi } from "./AuthLoginFormUi";
|
@ -3,19 +3,19 @@ import React, { useState, useEffect } from "react";
|
||||
import { BreadCrumbsUi } from "../../shared/UI/BreadCrumbsUi";
|
||||
import {ButtonUi, ButtonUiType} from "../../shared/UI/ButtonUi";
|
||||
import { LoaderUi } from "../../shared/UI/LoaderUi/LoaderUi";
|
||||
import { DefaultDropDown } from "../../entities/DefaultDropDown";
|
||||
import { AuctionFilters } from "../../widgets/AuctionFilters";
|
||||
import { DefaultPagination } from "../../entities/DefaultPagination";
|
||||
import { AddAuctionModal } from "../../widgets/AddAuctionModal";
|
||||
import { EditAuctionModal } from "../../widgets/EditAuctionModal";
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import { SearchUi } from "../../entities/Ui/SearchUi";
|
||||
|
||||
import Table from 'react-bootstrap/Table';
|
||||
|
||||
import { AuctionItem } from "../../types";
|
||||
import { filterItems } from "../../constants/data";
|
||||
|
||||
import { api } from "../../query/query";
|
||||
import { getCookie } from "typescript-cookie";
|
||||
|
||||
import loupe from "../../shared/images/loupe.png"
|
||||
import filterImg from "../../shared/images/filter.png"
|
||||
import close from "../../shared/images/close.png"
|
||||
import edit from "../../shared/images/edit.png"
|
||||
|
||||
import styles from "./auctionPage.module.scss"
|
||||
@ -24,13 +24,7 @@ export const AuctionPage = () => {
|
||||
|
||||
const [auctionItems, setAuctionItems] = useState<AuctionItem[]>([])
|
||||
|
||||
const [openAddModal, setOpenAddModal] = useState(false)
|
||||
|
||||
const [loader, setLoader] = useState(false)
|
||||
|
||||
const [openEditModal, setOpenEditModal] = useState(false)
|
||||
|
||||
const [currentEditAuction, setCurrentEditAuction] = useState({
|
||||
const [currentEditAuction, setCurrentEditAuction] = useState<AuctionItem>({
|
||||
uuid: '018c448e-c7d7-7092-b151-08f8d8bc410e',
|
||||
dateCreate: "2023-12-07 16:54:17",
|
||||
name: '',
|
||||
@ -41,6 +35,12 @@ export const AuctionPage = () => {
|
||||
status: 'Сбор заявок'
|
||||
})
|
||||
|
||||
const [openAddModal, setOpenAddModal] = useState(false)
|
||||
|
||||
const [loader, setLoader] = useState(false)
|
||||
|
||||
const [openEditModal, setOpenEditModal] = useState(false)
|
||||
|
||||
const addNewAuction = (newAction: string) => {
|
||||
setAuctionItems((prevValue) => [...prevValue, {
|
||||
uuid: '018c448e-c7d7-7092-b151-08f8d8bc480e',
|
||||
@ -91,7 +91,7 @@ export const AuctionPage = () => {
|
||||
setAuctionItems(res.data)
|
||||
setLoader(false)
|
||||
})
|
||||
}, [])
|
||||
}, [getCookie('authToken')])
|
||||
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
@ -102,33 +102,15 @@ export const AuctionPage = () => {
|
||||
<div className={styles.home__info}>
|
||||
<div className={styles.info__top}>
|
||||
<ButtonUi event={() => setOpenAddModal(true)} title={'+ Добавить аукцион'} variant={ButtonUiType.PRIMARY} />
|
||||
<div className={styles.info__search}>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Найти аукцион"
|
||||
/>
|
||||
<img src={loupe} alt='loupe' />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.info__filters}>
|
||||
<img className={styles.info__filtersImg} src={filterImg} alt="filter" />
|
||||
<div className={styles.info__filters__items}>
|
||||
{filterItems.map((item) => {
|
||||
return <DefaultDropDown key={item.title} title={item.title} options={item.options} />
|
||||
})}
|
||||
</div>
|
||||
<ButtonUi
|
||||
title={'Сбросить фильтры'}
|
||||
variant={ButtonUiType.PRIMARY}
|
||||
img={<img src={close} alt="cross" />} />
|
||||
<ButtonUi title={'Применить'} variant={ButtonUiType.INFO} />
|
||||
<SearchUi />
|
||||
</div>
|
||||
<AuctionFilters />
|
||||
<div className={styles.info__tableWrapper}>
|
||||
{loader ?
|
||||
<LoaderUi animation={'border'} variant={'primary'} />
|
||||
:
|
||||
<>
|
||||
<table>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr className={styles.tableItem}>
|
||||
<td>№</td>
|
||||
@ -155,7 +137,7 @@ export const AuctionPage = () => {
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
<DefaultPagination pageCount={10} currentPage={2} />
|
||||
</>
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
&__info {
|
||||
width: 100%;
|
||||
margin: 35px 0;
|
||||
min-height: 168px;
|
||||
min-height: 281px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
@ -23,8 +23,8 @@
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
border-radius: 8px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
width: 3px;
|
||||
height: 5px;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
@ -36,79 +36,8 @@
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid gray;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
border: none;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
padding: 10px 15px;
|
||||
background-color: #2f95f2;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 1213px;
|
||||
|
||||
&Img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 17px;
|
||||
max-width: 200px;
|
||||
border: none;
|
||||
color: white;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
column-gap: 8px;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
&__reset {
|
||||
background-color: #0668c2;
|
||||
}
|
||||
|
||||
&__confirm {
|
||||
background-color: #00529d;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__tableWrapper {
|
||||
min-width: 1243px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 1213px;
|
||||
|
||||
.tableItem {
|
||||
padding: 10px;
|
||||
@ -123,6 +52,8 @@
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
|
||||
img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@ -133,34 +64,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
thead {
|
||||
display: grid;
|
||||
|
||||
tr {
|
||||
display: grid;
|
||||
grid-template-columns: 30% 20% 16% 12% 14% 8%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: grid;
|
||||
row-gap: 10px;
|
||||
|
||||
tr {
|
||||
display: grid;
|
||||
grid-template-columns: 30% 20% 16% 12% 14% 8%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,99 +1,15 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { ButtonUi, ButtonUiType } from "../../shared/UI/ButtonUi";
|
||||
|
||||
import Form from 'react-bootstrap/Form';
|
||||
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { api } from "../../query/query";
|
||||
import { setCookie } from "typescript-cookie";
|
||||
import { AuthResponsePayload } from "../../types";
|
||||
import { AuthLoginFormUi } from "../../features/auth/ui/AuthLoginFormUi";
|
||||
|
||||
import styles from "./authPage.module.scss"
|
||||
|
||||
export const AuthPage = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const schema = yup.object().shape({
|
||||
username: yup.string()
|
||||
.min(3, 'Минимум 3 символа!')
|
||||
.max(16, 'Максимум 16 символов!')
|
||||
.required('Это поле обязательное.'),
|
||||
|
||||
password: yup.string().required('Это обязательное поле.').min(6, 'Минимум 6 символов!'),
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
errors,
|
||||
values,
|
||||
setFieldError
|
||||
} = useFormik({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
validationSchema: schema,
|
||||
onSubmit: async (values): Promise<AuthResponsePayload> => {
|
||||
return await api.post('/authorization', {
|
||||
username: values.username,
|
||||
password: values.password
|
||||
}).then((res) => {
|
||||
if (res.data[0]) {
|
||||
setFieldError('username', res.data[0])
|
||||
setFieldError('password', res.data[0])
|
||||
} else {
|
||||
setCookie('authToken', res.data!.authToken)
|
||||
setCookie('refreshToken', res.data!.refreshToken)
|
||||
navigate("/auction")
|
||||
}
|
||||
return res.data as AuthResponsePayload
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.auth}>
|
||||
<h1 className={styles.auth__title}>Аукционная площадка для проведения закупок ГК Проф-Пресс</h1>
|
||||
<Form noValidate className={styles.auth__form} onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="username"
|
||||
placeholder="username"
|
||||
onChange={handleChange}
|
||||
name="username"
|
||||
onBlur={handleBlur}
|
||||
value={values.username}
|
||||
isInvalid={!!errors.username}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{errors.username}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
type="password"
|
||||
id="password"
|
||||
placeholder="password"
|
||||
onChange={handleChange}
|
||||
value={values.password}
|
||||
name="password"
|
||||
onBlur={handleBlur}
|
||||
isInvalid={!!errors.password}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{errors.password}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<ButtonUi title={'Войти'} variant={ButtonUiType.PRIMARY} type={'submit'}/>
|
||||
</Form >
|
||||
<AuthLoginFormUi />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -10,13 +10,4 @@
|
||||
max-width: 700px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__form {
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ api.interceptors.response.use(undefined, async function (error) {
|
||||
if (error?.response?.status === 401) {
|
||||
removeCookie('authToken')
|
||||
removeCookie('refreshToken')
|
||||
window.location.replace("/auth")
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
28
src/widgets/AuctionFilters/AuctionFilters.tsx
Normal file
28
src/widgets/AuctionFilters/AuctionFilters.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
import { DefaultDropDown } from "../../entities/DefaultDropDown";
|
||||
import { ButtonUi, ButtonUiType } from "../../shared/UI/ButtonUi";
|
||||
|
||||
import { filterItems } from "../../constants/data";
|
||||
|
||||
import filterImg from "../../shared/images/filter.png";
|
||||
import close from "../../shared/images/close.png";
|
||||
import styles from "./auctionFilters.module.scss";
|
||||
|
||||
export const AuctionFilters = () => {
|
||||
return (
|
||||
<div className={styles.filter}>
|
||||
<img className={styles.filter__img} src={filterImg} alt="filter" />
|
||||
<div className={styles.filter__items}>
|
||||
{filterItems.map((item) => {
|
||||
return <DefaultDropDown key={item.title} title={item.title} options={item.options} />
|
||||
})}
|
||||
</div>
|
||||
<ButtonUi
|
||||
title={'Сбросить фильтры'}
|
||||
variant={ButtonUiType.PRIMARY}
|
||||
img={<img src={close} alt="cross" />} />
|
||||
<ButtonUi title={'Применить'} variant={ButtonUiType.INFO} />
|
||||
</div>
|
||||
)
|
||||
}
|
36
src/widgets/AuctionFilters/auctionFilters.module.scss
Normal file
36
src/widgets/AuctionFilters/auctionFilters.module.scss
Normal file
@ -0,0 +1,36 @@
|
||||
.filter {
|
||||
display: flex;
|
||||
padding: 10px 15px;
|
||||
background-color: #2f95f2;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 1213px;
|
||||
|
||||
&__img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 17px;
|
||||
max-width: 200px;
|
||||
border: none;
|
||||
color: white;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
column-gap: 8px;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
}
|
||||
}
|
1
src/widgets/AuctionFilters/index.ts
Normal file
1
src/widgets/AuctionFilters/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AuctionFilters } from "./AuctionFilters"
|
Loading…
Reference in New Issue
Block a user