Compare commits

...

10 Commits

Author SHA1 Message Date
Mikola
efc1dde782 fixes 2023-12-20 21:01:04 +03:00
Mikola
02886529f1 Merge remote-tracking branch 'origin/main' 2023-12-20 21:00:35 +03:00
Mikola
4680ea6701 fixes 2023-12-20 21:00:20 +03:00
artemsaga
da0e0f774b docker 2023-12-20 20:18:32 +03:00
Mikola
319ee611fb Merge remote-tracking branch 'origin/main' 2023-12-20 19:17:31 +03:00
Mikola
7d29222534 fixes 2023-12-20 19:16:35 +03:00
artemsaga
7d07651323 Merge remote-tracking branch 'origin/main' into main 2023-12-20 19:10:26 +03:00
artemsaga
cd2be9a53a docker 2023-12-20 19:10:17 +03:00
Mikola
b3e018fa70 fixes 2023-12-20 18:14:46 +03:00
Mikola
88a676f6fb auction api 2023-12-19 16:23:03 +03:00
24 changed files with 10425 additions and 313 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
NGINX_PORT=8090

20
Makefile Normal file
View 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
View 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

View File

@ -0,0 +1,9 @@
server {
listen 80;
index index.html;
root /app/build;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
}

View File

@ -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
View 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" />;
}

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

View File

@ -0,0 +1 @@
export { SearchUi } from "./SearchUi"

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

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

View File

@ -0,0 +1,8 @@
.container {
padding: 20px 0;
display: flex;
flex-direction: column;
row-gap: 15px;
width: 100%;
max-width: 400px;
}

View File

@ -0,0 +1 @@
export { AuthLoginFormUi } from "./AuthLoginFormUi";

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

@ -0,0 +1 @@
export { AuctionFilters } from "./AuctionFilters"

View File

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

10016
yarn.lock Normal file

File diff suppressed because it is too large Load Diff