Compare commits
10 Commits
f27d98607e
...
efc1dde782
Author | SHA1 | Date | |
---|---|---|---|
|
efc1dde782 | ||
|
02886529f1 | ||
|
4680ea6701 | ||
|
da0e0f774b | ||
|
319ee611fb | ||
|
7d29222534 | ||
|
7d07651323 | ||
|
cd2be9a53a | ||
|
b3e018fa70 | ||
|
88a676f6fb |
20
Makefile
Normal file
20
Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
up: docker-up
|
||||
down: docker-down
|
||||
restart: docker-down docker-up
|
||||
init: docker-down docker-pull docker-build docker-up assets-install
|
||||
|
||||
docker-up:
|
||||
docker compose -f ./docker-compose.yml --compatibility up -d
|
||||
|
||||
docker-down:
|
||||
docker compose -f ./docker-compose.yml down --remove-orphans
|
||||
|
||||
docker-pull:
|
||||
docker compose -f ./docker-compose.yml pull
|
||||
|
||||
docker-build:
|
||||
docker compose -f ./docker-compose.yml build
|
||||
|
||||
assets-install:
|
||||
docker compose -f ./docker-compose.yml run --rm node yarn install
|
||||
docker compose -f ./docker-compose.yml run --rm node yarn run build
|
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
nginx:
|
||||
image: library/nginx:1-alpine
|
||||
volumes:
|
||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./:/app
|
||||
working_dir: /app
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
restart: unless-stopped
|
||||
node:
|
||||
image: library/node:20-alpine
|
||||
volumes:
|
||||
- ./:/app
|
||||
working_dir: /app
|
9
docker/nginx/default.conf
Normal file
9
docker/nginx/default.conf
Normal file
@ -0,0 +1,9 @@
|
||||
server {
|
||||
listen 80;
|
||||
index index.html;
|
||||
root /app/build;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
}
|
@ -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 { ButtonUi, ButtonUiType } from "../../../../shared/UI/ButtonUi";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
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";
|
@ -1,81 +1,62 @@
|
||||
import React, {useState} from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { BreadCrumbsUi } from "../../shared/UI/BreadCrumbsUi";
|
||||
import {ButtonUi, ButtonUiType} from "../../shared/UI/ButtonUi";
|
||||
import { DefaultDropDown } from "../../entities/DefaultDropDown";
|
||||
import { LoaderUi } from "../../shared/UI/LoaderUi/LoaderUi";
|
||||
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 { AuctionItem } from "../../types";
|
||||
import { filterItems } from "../../constants/data";
|
||||
import { SearchUi } from "../../entities/Ui/SearchUi";
|
||||
|
||||
import Table from 'react-bootstrap/Table';
|
||||
|
||||
import { AuctionItem } from "../../types";
|
||||
|
||||
import { api } from "../../query/query";
|
||||
|
||||
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"
|
||||
|
||||
export const AuctionPage = () => {
|
||||
|
||||
const [auctionItems, setAuctionItems] = useState<AuctionItem[]>([
|
||||
{
|
||||
number: 1,
|
||||
name: 'Аукцион на закупку в интересах компании ООО "Фабрика"',
|
||||
receptionDate: '17.04.23-18.04.23',
|
||||
startDate: '18.04.23',
|
||||
status: 'Черновик'
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
name: 'Аукцион на закупку в интересах компании ООО "Пресс"',
|
||||
receptionDate: '17.04.23-18.04.23',
|
||||
startDate: '18.04.23',
|
||||
status: 'Сбор заявок'
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
name: 'Аукцион на закупку в интересах компании ООО "Компания"',
|
||||
receptionDate: '17.04.23-18.04.23',
|
||||
startDate: '18.04.23',
|
||||
status: 'Идут торги'
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
name: 'Аукцион на закупку в интересах компании ООО "Кот"',
|
||||
receptionDate: '17.04.23-18.04.23',
|
||||
startDate: '18.04.23',
|
||||
status: 'В архиве'
|
||||
},
|
||||
])
|
||||
const [auctionItems, setAuctionItems] = useState<AuctionItem[]>([])
|
||||
|
||||
const [currentEditAuction, setCurrentEditAuction] = useState<AuctionItem>({
|
||||
uuid: '018c448e-c7d7-7092-b151-08f8d8bc410e',
|
||||
dateCreate: "2023-12-07 16:54:17",
|
||||
name: '',
|
||||
auctionStartDate: "2023-12-22 16:03:00",
|
||||
description: "",
|
||||
requestsEndDate: "2023-12-21 16:03:00",
|
||||
requestsStartDate: "2023-12-18 16:06:00",
|
||||
status: 'Сбор заявок'
|
||||
})
|
||||
|
||||
const [openAddModal, setOpenAddModal] = useState(false)
|
||||
|
||||
const [openEditModal, setOpenEditModal] = useState(false)
|
||||
const [loader, setLoader] = useState(false)
|
||||
|
||||
const [currentEditAuction, setCurrentEditAuction] = useState({
|
||||
number: 0,
|
||||
name: '',
|
||||
receptionDate: '',
|
||||
startDate: '',
|
||||
status: ''
|
||||
})
|
||||
const [openEditModal, setOpenEditModal] = useState(false)
|
||||
|
||||
const addNewAuction = (newAction: string) => {
|
||||
setAuctionItems((prevValue) => [...prevValue, {
|
||||
number: prevValue.length + 1,
|
||||
uuid: '018c448e-c7d7-7092-b151-08f8d8bc480e',
|
||||
dateCreate: "2023-12-07 16:54:17",
|
||||
name: newAction,
|
||||
receptionDate: '17.04.23-18.04.23',
|
||||
startDate: '18.04.23',
|
||||
auctionStartDate: "2023-12-22 16:03:00",
|
||||
description: "",
|
||||
requestsEndDate: "2023-12-21 16:03:00",
|
||||
requestsStartDate: "2023-12-18 16:06:00",
|
||||
status: 'Сбор заявок'
|
||||
}])
|
||||
}
|
||||
|
||||
const editAuctionItem = (newAuctionName: string, currentEditAuctionId: number) => {
|
||||
const editAuctionItem = (newAuctionName: string, currentEditAuctionId: string) => {
|
||||
setAuctionItems((prevValue) => {
|
||||
return prevValue.map((item) => {
|
||||
if (item.number === currentEditAuctionId) {
|
||||
if (item.uuid === currentEditAuctionId) {
|
||||
return {...item, name: newAuctionName}
|
||||
}
|
||||
return item
|
||||
@ -83,6 +64,34 @@ export const AuctionPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const getCorrectDate = (day:string) => {
|
||||
const months = [
|
||||
"января",
|
||||
"февраля",
|
||||
"марта",
|
||||
"апреля",
|
||||
"мая",
|
||||
"июня",
|
||||
"июля",
|
||||
"августа",
|
||||
"сентября",
|
||||
"октября",
|
||||
"ноября",
|
||||
"декабря",
|
||||
];
|
||||
return `${new Date(day).getDate()} ${
|
||||
months[new Date(day).getMonth()]
|
||||
} ${new Date(day).getFullYear()} года`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoader(true)
|
||||
api.get('/auctions').then((res) => {
|
||||
setAuctionItems(res.data)
|
||||
setLoader(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<BreadCrumbsUi links={[
|
||||
@ -92,57 +101,45 @@ 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}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className={styles.tableItem}>
|
||||
<td>№</td>
|
||||
<td>Название аукциона</td>
|
||||
<td>Прием заявок</td>
|
||||
<td>Проведение</td>
|
||||
<td>Статус</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{auctionItems.map((item) => {
|
||||
return <tr className={styles.tableItem} key={item.number}>
|
||||
<td>{item.number}</td>
|
||||
<td className={styles.tableItem__name}>{item.name}</td>
|
||||
<td>{item.receptionDate}</td>
|
||||
<td>{item.startDate}</td>
|
||||
<td>{item.status}</td>
|
||||
<td className={styles.tableItem__edit}>
|
||||
<img src={edit} alt="edit" onClick={() => {
|
||||
setCurrentEditAuction(item)
|
||||
setOpenEditModal(true)
|
||||
}}/>
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<DefaultPagination pageCount={10} currentPage={2} />
|
||||
{loader ?
|
||||
<LoaderUi animation={'border'} variant={'primary'} />
|
||||
:
|
||||
<>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr className={styles.tableItem}>
|
||||
<td>№</td>
|
||||
<td>Название аукциона</td>
|
||||
<td>Прием заявок</td>
|
||||
<td>Проведение</td>
|
||||
<td>Статус</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{auctionItems.map((item) => {
|
||||
return <tr className={styles.tableItem} key={item.uuid}>
|
||||
<td>{item.uuid}</td>
|
||||
<td className={styles.tableItem__name}>{item.name}</td>
|
||||
<td>{getCorrectDate((item.requestsStartDate))}</td>
|
||||
<td>{item.dateCreate}</td>
|
||||
<td>{item.status}</td>
|
||||
<td className={styles.tableItem__edit}>
|
||||
<img src={edit} alt="edit" onClick={() => {
|
||||
setCurrentEditAuction(item)
|
||||
setOpenEditModal(true)
|
||||
}}/>
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
<DefaultPagination pageCount={10} currentPage={2} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<AddAuctionModal
|
||||
|
@ -10,6 +10,7 @@
|
||||
&__info {
|
||||
width: 100%;
|
||||
margin: 35px 0;
|
||||
min-height: 281px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
@ -22,8 +23,8 @@
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
border-radius: 8px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
width: 3px;
|
||||
height: 5px;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
@ -35,76 +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;
|
||||
min-width: 1213px;
|
||||
|
||||
.tableItem {
|
||||
padding: 10px;
|
||||
@ -119,6 +52,8 @@
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
|
||||
img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@ -129,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: 7% 50% 16% 12% 10% 5%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: grid;
|
||||
row-gap: 10px;
|
||||
|
||||
tr {
|
||||
display: grid;
|
||||
grid-template-columns: 7% 50% 16% 12% 10% 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { removeCookie } from 'typescript-cookie'
|
||||
import { removeCookie, getCookie } from 'typescript-cookie'
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: 'https://tender.prof-press.ru/api/v1/',
|
||||
@ -9,10 +9,16 @@ export const api = axios.create({
|
||||
},
|
||||
})
|
||||
|
||||
api.interceptors.request.use(config => {
|
||||
config.headers.authorization = getCookie('authToken') ? `Bearer ${getCookie('authToken')}` : ''
|
||||
return config
|
||||
})
|
||||
|
||||
api.interceptors.response.use(undefined, async function (error) {
|
||||
if (error?.response?.status === 401) {
|
||||
removeCookie('access-token')
|
||||
removeCookie('refresh-token')
|
||||
removeCookie('authToken')
|
||||
removeCookie('refreshToken')
|
||||
window.location.replace("/auth")
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
14
src/shared/UI/LoaderUi/LoaderUi.tsx
Normal file
14
src/shared/UI/LoaderUi/LoaderUi.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
|
||||
interface LoaderUi {
|
||||
animation: 'border' | 'grow',
|
||||
variant: string
|
||||
}
|
||||
|
||||
export const LoaderUi:React.FC<LoaderUi> = ({animation, variant}) => {
|
||||
return (
|
||||
<Spinner animation={animation} variant={variant} />
|
||||
)
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
export interface AuctionItem {
|
||||
number: number,
|
||||
uuid: string,
|
||||
name: string,
|
||||
receptionDate: string,
|
||||
startDate: string,
|
||||
description: string,
|
||||
requestsStartDate: string,
|
||||
requestsEndDate: string,
|
||||
auctionStartDate: string,
|
||||
dateCreate: string
|
||||
status: string
|
||||
}
|
||||
|
||||
|
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"
|
@ -3,20 +3,13 @@ import React, {useState, useEffect} from "react";
|
||||
import { ButtonUi, ButtonUiType } from "../../shared/UI/ButtonUi";
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
|
||||
type auctionItem = {
|
||||
number: number,
|
||||
name: string,
|
||||
receptionDate: string,
|
||||
startDate: string,
|
||||
status: string
|
||||
}
|
||||
import { AuctionItem } from "../../types";
|
||||
|
||||
interface AddAuctionModalProps {
|
||||
showModal: boolean,
|
||||
onHide: () => void,
|
||||
currentAuction: auctionItem,
|
||||
editAuctionItem: (newAuctionName:string, currentEditAuctionId:number) => void
|
||||
currentAuction: AuctionItem,
|
||||
editAuctionItem: (newAuctionName:string, currentEditAuctionId:string) => void
|
||||
}
|
||||
|
||||
export const EditAuctionModal:React.FC<AddAuctionModalProps> = ({showModal, onHide, currentAuction, editAuctionItem}) => {
|
||||
@ -27,7 +20,7 @@ export const EditAuctionModal:React.FC<AddAuctionModalProps> = ({showModal, onHi
|
||||
}, [currentAuction])
|
||||
|
||||
const editAuction = () => {
|
||||
editAuctionItem(newAuctionName, currentAuction.number)
|
||||
editAuctionItem(newAuctionName, currentAuction.uuid)
|
||||
onHide()
|
||||
}
|
||||
return (
|
||||
@ -40,7 +33,7 @@ export const EditAuctionModal:React.FC<AddAuctionModalProps> = ({showModal, onHi
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title id="contained-modal-title-vcenter">
|
||||
Редактировать аукцион №{currentAuction.number}
|
||||
Редактировать аукцион №{currentAuction.uuid}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
|
Loading…
Reference in New Issue
Block a user