Fixed copmonents

This commit is contained in:
MaxOvs19 2023-05-24 15:34:43 +03:00
parent 08f7d13f01
commit 2ad78834f3
41 changed files with 837 additions and 847 deletions

View File

@ -1,7 +1,7 @@
const path = require('path');
const path = require("path");
module.exports = {
public: path.resolve(__dirname, '../public'),
src: path.resolve(__dirname, '../src'),
build: path.resolve(__dirname, '../build'),
'@node_modules': path.resolve(__dirname, '../node_modules'),
public: path.resolve(__dirname, "../public"),
src: path.resolve(__dirname, "../src"),
build: path.resolve(__dirname, "../build"),
"@node_modules": path.resolve(__dirname, "../node_modules"),
};

View File

@ -1,10 +1,10 @@
const { merge } = require('webpack-merge');
const { merge } = require("webpack-merge");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const prod = require('./prod');
const prod = require("./prod");
module.exports = merge(prod, {
plugins: [new BundleAnalyzerPlugin()]
plugins: [new BundleAnalyzerPlugin()],
});

View File

@ -1,14 +1,14 @@
const paths = require('../paths');
const paths = require("../paths");
const webpack = require('webpack');
const {merge} = require('webpack-merge');
const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require('./common');
const common = require("./common");
module.exports = merge(common, {
target : 'web',
mode: 'development',
devtool: 'eval-cheap-source-map',
target: "web",
mode: "development",
devtool: "eval-cheap-source-map",
devServer: {
compress: true,
@ -17,7 +17,6 @@ module.exports = merge(common, {
historyApiFallback: true,
// open: true,
port: 3000,
},
plugins: [new webpack.HotModuleReplacementPlugin()]
plugins: [new webpack.HotModuleReplacementPlugin()],
});

View File

@ -1,26 +1,25 @@
const paths = require('../paths');
const {merge} = require('webpack-merge');
const common = require('./common');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const paths = require("../paths");
const { merge } = require("webpack-merge");
const common = require("./common");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = merge(common, {
mode: 'production',
target :'browserslist',
mode: "production",
target: "browserslist",
entry: {
index: {
import: `${paths.src}/index.js`,
dependOn: ['react', 'helpers']
dependOn: ["react", "helpers"],
},
react: ['react', 'react-dom', 'prop-types'],
helpers: ['immer', 'nanoid']
react: ["react", "react-dom", "prop-types"],
helpers: ["immer", "nanoid"],
},
devtool: false,
output: {
filename: 'js/[name].[hash:8].bundle.js',
publicPath: '/',
assetModuleFilename: '[hash][ext][query]'
filename: "js/[name].[hash:8].bundle.js",
publicPath: "/",
assetModuleFilename: "[hash][ext][query]",
},
module: {
rules: [
@ -29,34 +28,32 @@ module.exports = merge(common, {
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {importLoaders: 1}
loader: "css-loader",
options: { importLoaders: 1 },
},
'postcss-loader',
'sass-loader'
]
"postcss-loader",
"sass-loader",
],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
type: 'asset/resource'
test: /\.(jpe?g|png|gif|svg|webp)$/i,
type: "asset/resource",
// type: 'asset'
},
]
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].css'
filename: "[name].[contenthash].css",
chunkFilename: "[id].css",
}),
],
optimization: {
runtimeChunk: 'single'
runtimeChunk: "single",
},
performance: {
hints: 'warning',
hints: "warning",
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
maxAssetSize: 512000,
},
});

View File

@ -1,19 +1,19 @@
import React from 'react'
import React from "react";
import './achievement.scss'
import "./achievement.scss";
export const Achievement = ({ achievement }) => {
return (
<div className='achievement'>
<div className='achievement__icon'>
<img src={achievement.img} alt='achievement' />
<div className="achievement">
<div className="achievement__icon">
<img src={achievement.img} alt="achievement" />
</div>
<div className='achievement__popup'>
<div className='achievement__title'>{achievement.title}</div>
<div className='achievement__description'>
<div className="achievement__popup">
<div className="achievement__title">{achievement.title}</div>
<div className="achievement__description">
{achievement.description}
</div>
</div>
</div>
)
}
);
};

View File

@ -3,7 +3,7 @@ import { calendarHelper, currentMonthAndDay } from "./calendarHelper";
import ellipse from "../../images/ellipse.png";
import rectangle from "../../images/rectangle__calendar.png";
import calendarIcon from "../../images/calendar_icon.png";
import calendarIcon from "../../images/calendar.svg";
import moment from "moment";
import "moment/locale/ru";

View File

@ -18,7 +18,6 @@ import { apiRequest } from "../../api/request";
import { createMarkup } from "../../helper";
import gitImgItem from "../../images/gitItemImg.svg";
import rectangle from "../../images/rectangle_secondPage.png";
import front from "../../images/front-end.webp";
import back from "../../images/back-end.webp";

View File

@ -2,7 +2,7 @@
max-width: 353px;
width: 100%;
padding: 35px 45px 15px 30px;
background: #FFFFFF;
background: #ffffff;
border-radius: 12px;
text-decoration: none;
cursor: pointer;
@ -56,14 +56,14 @@
margin-bottom: 0;
span {
color: #52B709;
color: #52b709;
font-weight: 700;
}
}
&Link {
width: 48px;
height: 48px;
background: #DDEEC6;
background: #ddeec6;
border-radius: 50px;
display: flex;
justify-content: center;

View File

@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
padding: 33px 32px 25px 28px;
background: #FFFFFF;
background: #ffffff;
border-radius: 12px;
transition: all 0.3s ease;
position: relative;
@ -18,7 +18,6 @@
pointer-events: none;
}
&__title {
display: flex;
align-items: center;
@ -43,7 +42,7 @@
p {
max-width: 181px;
margin-bottom: 0;
color: #6F6F6F;
color: #6f6f6f;
font-weight: 400;
font-size: 12px;
line-height: 20px;
@ -55,7 +54,7 @@
justify-content: center;
width: 48px;
height: 48px;
background: #DDEEC6;
background: #ddeec6;
border-radius: 50px;
}
}

View File

@ -9,9 +9,8 @@ import { selectProfiles } from "../../redux/outstaffingSlice";
import { urlForLocal } from "../../helper";
import male from "../../images/medium_male.png";
import rectangle from "../../images/rectangle_secondPage.png";
import cursorImg from "../../images/cursorImg.png";
import cursorImg from "../../images/cursorImg.svg";
import "./description.scss";

View File

@ -1,49 +0,0 @@
import React from 'react'
import logo from '../../images/logoGuild.png'
import vk from '../../images/vkLogo.svg'
import tg from '../../images/tgFooter.png'
import email from '../../images/emailLogo.svg'
import './footer.scss'
export const Footer = () => {
return (
<footer>
<div className='container'>
<div className='footer'>
<div className='footer__top'>
<img src={logo} alt='logo' />
<p>Подберем и документально оформим IT-специалистов, после чего передадим исполнителей под ваше руководство.
Вы получаете полное управление над сотрудниками, имея возможность контролировать и заменять IT штат.</p>
<div className='footer__copyright'>
© {new Date().getFullYear()} - Все права защищены
</div>
</div>
<div className='footer__bottom'>
<div className='footer__social'>
<div className='footer__social__icons'>
<a>
<img src={vk} alt='vk' />
</a>
<a>
<img src={tg} alt='tg' />
</a>
</div>
<p>Войти в команду</p>
</div>
<div className='footer__info'>
<div className='footer__mail'>
<a>
<img src={email} alt='email' />
</a>
<p>office@itguild.info</p>
</div>
<a className='footer__policy'>Политика конфиденциальности</a>
</div>
</div>
</div>
</div>
</footer>
)
}

View File

@ -0,0 +1,53 @@
import React from "react";
import logo from "../../images/logoGuild.png";
import vk from "../../images/vkLogo.svg";
import tg from "../../images/tgFooter.png";
import email from "../../images/emailLogo.svg";
import "./footer.scss";
export const Footer = () => {
return (
<footer>
<div className="container">
<div className="footer">
<div className="footer__top">
<img src={logo} alt="logo" />
<p>
Подберем и документально оформим IT-специалистов, после чего
передадим исполнителей под ваше руководство. Вы получаете полное
управление над сотрудниками, имея возможность контролировать и
заменять IT штат.
</p>
<div className="footer__copyright">
© {new Date().getFullYear()} - Все права защищены
</div>
</div>
<div className="footer__bottom">
<div className="footer__social">
<div className="footer__social__icons">
<a>
<img src={vk} alt="vk" />
</a>
<a>
<img src={tg} alt="tg" />
</a>
</div>
<p>Войти в команду</p>
</div>
<div className="footer__info">
<div className="footer__mail">
<a>
<img src={email} alt="email" />
</a>
<p>office@itguild.info</p>
</div>
<a className="footer__policy">Политика конфиденциальности</a>
</div>
</div>
</div>
</div>
</footer>
);
};

View File

@ -4,7 +4,6 @@ footer {
}
.footer {
&__top {
display: flex;
align-items: start;
@ -21,7 +20,7 @@ footer {
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: #5B6871;
color: #5b6871;
@media (max-width: 620px) {
margin-left: 0;
@ -82,7 +81,7 @@ footer {
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: #5B6871;
color: #5b6871;
}
}
@ -90,7 +89,7 @@ footer {
font-weight: 400;
font-size: 10px;
line-height: 16px;
color: #5B6871;
color: #5b6871;
margin-left: 150px;
@media (max-width: 720px) {
@ -98,7 +97,7 @@ footer {
}
&:hover {
color: #5B6871;
color: #5b6871;
text-decoration: none;
}
}
@ -115,101 +114,4 @@ footer {
margin-left: 0;
}
}
//margin-top: -3rem;
//
//&__left {
// display: flex;
// align-items: center;
//}
//
//&__description {
// padding: 0 100px 0 34px;
//
// span {
// color: #18586e;
// font-family: 'GT Eesti Pro Display';
// font-size: 1.6em;
// font-weight: 400;
// font-style: normal;
// letter-spacing: normal;
// line-height: 16.81px;
// text-align: left;
// }
//}
//
//&__icon {
// text-align: end;
//
// img {
// margin-left: 20px;
// }
//}
//
//&__right {
// display: flex;
// flex-direction: column;
// align-items: left;
//}
//
//&__phone {
// color: #003b65;
// font-family: 'CeraPro';
// font-size: 2.1em;
// letter-spacing: normal;
// line-height: 25px;
// text-align: left;
//}
//
//&__working-hours {
// color: #003b65;
// font-family: 'CeraPro';
// font-size: 1.2em;
// font-weight: 400;
// font-style: normal;
// letter-spacing: normal;
// line-height: normal;
// margin-left: 24px;
//}
//
//&__copyright {
// padding: 1rem 1rem 1rem 5.6rem;
// font-family: 'Muller';
// font-weight: 300;
// font-size: 1.2em;
//}
}
//@media (max-width: 1199px) {
// .footer {
// &__left {
// margin-bottom: 20px;
// }
// }
//}
//
//@media (max-width: 575.98px) {
// .footer {
// &__left {
// margin-top: 80px;
// }
//
// &__description {
// padding: 0;
// margin-left: 10px;
//
// span {
// font-size: 1.2em;
// }
// }
//
// &__icon {
// img {
// margin-left: 10px;
// }
// }
//
// &__right {
// margin-bottom: 20px;
// }
// }
//}

View File

@ -1,134 +0,0 @@
import React, {useEffect, useState} from 'react'
import {useParams, useNavigate} from 'react-router-dom'
import {Loader} from '../Loader/Loader'
import PhoneInput from 'react-phone-input-2'
import 'react-phone-input-2/lib/style.css'
import './form.scss'
import {apiRequest} from "../../api/request";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content'
const SweetAlert = withReactContent(Swal);
const Form = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [status, setStatus] = useState(null);
const [data, setData] = useState({
email: '',
phone: '',
comment: ''
});
const [isFetching, setIsFetching] = useState(false);
const handleModal = (status) => {
SweetAlert.fire({
text: status !== 200 || 201
? 'Какие-то неполадки =('
: 'Форма отправлена',
preConfirm: () =>
status !== 200 || 201 ? () => {
setStatus(null)
} : () => {
setStatus(null);
navigate(`/candidate/${urlParams.id}`)
}
});
};
useEffect(() => {
if (status) {
handleModal(status)
}
}, [status]);
const handleChange = (e) => {
const {id, value} = e.target;
setData((prev) => ({
...prev,
[id]: value
}))
};
const handleSubmit = (e) => {
e.preventDefault();
setIsFetching(true);
const formData = new FormData();
formData.append('profile_id', urlParams.id);
formData.append('Email', data.email);
formData.append('phone', data.phone);
formData.append('comment', data.comment);
apiRequest('/interview-request/create-interview-request', {
method: 'POST',
params: {
profile_id: urlParams.id,
...data
}
}).then((res) => {
setStatus(res);
setIsFetching(false)
}
)
};
return (
<div className='row'>
<div className='col-sm-12'>
<form className='form' id='test'>
<label htmlFor='email'>Емейл:</label>
<input
onChange={handleChange}
id='email'
name='Email'
type='email'
placeholder='Емейл'
value={data.email}
/>
<label htmlFor='phone'>Номер телефона:</label>
<PhoneInput
id='phone'
name='Phone'
country={'ru'}
value={data.phone}
onChange={(e) =>
handleChange({target: {value: e, id: 'phone'}})
}
/>
{/* <input
onChange={handleChange}
id="phone"
type="text"
name="Phone"
placeholder="Телефон"
value={data.phone}
/> */}
<textarea
onChange={handleChange}
id='comment'
rows='5'
cols='40'
name='Comment'
placeholder='Оставьте комментарий'
value={data.comment}
></textarea>
<button onClick={handleSubmit} className='form__btn' type='submit'>
{isFetching ? <Loader/> : 'Отправить'}
</button>
</form>
</div>
</div>
)
};
export default Form

View File

@ -0,0 +1,125 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import PhoneInput from "react-phone-input-2";
import { apiRequest } from "../../api/request";
import { Loader } from "../Loader/Loader";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import "react-phone-input-2/lib/style.css";
import "./form.scss";
const SweetAlert = withReactContent(Swal);
const Form = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [status, setStatus] = useState(null);
const [data, setData] = useState({
email: "",
phone: "",
comment: "",
});
const [isFetching, setIsFetching] = useState(false);
const handleModal = (status) => {
SweetAlert.fire({
text:
status !== 200 || 201 ? "Какие-то неполадки =(" : "Форма отправлена",
preConfirm: () =>
status !== 200 || 201
? () => {
setStatus(null);
}
: () => {
setStatus(null);
navigate(`/candidate/${urlParams.id}`);
},
});
};
useEffect(() => {
if (status) {
handleModal(status);
}
}, [status]);
const handleChange = (e) => {
const { id, value } = e.target;
setData((prev) => ({
...prev,
[id]: value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
setIsFetching(true);
const formData = new FormData();
formData.append("profile_id", urlParams.id);
formData.append("Email", data.email);
formData.append("phone", data.phone);
formData.append("comment", data.comment);
apiRequest("/interview-request/create-interview-request", {
method: "POST",
params: {
profile_id: urlParams.id,
...data,
},
}).then((res) => {
setStatus(res);
setIsFetching(false);
});
};
return (
<div className="row">
<div className="col-sm-12">
<form className="form" id="test">
<label htmlFor="email">Емейл:</label>
<input
onChange={handleChange}
id="email"
name="Email"
type="email"
placeholder="Емейл"
value={data.email}
/>
<label htmlFor="phone">Номер телефона:</label>
<PhoneInput
id="phone"
name="Phone"
country={"ru"}
value={data.phone}
onChange={(e) =>
handleChange({ target: { value: e, id: "phone" } })
}
/>
<textarea
onChange={handleChange}
id="comment"
rows="5"
cols="40"
name="Comment"
placeholder="Оставьте комментарий"
value={data.comment}
></textarea>
<button onClick={handleSubmit} className="form__btn" type="submit">
{isFetching ? <Loader /> : "Отправить"}
</button>
</form>
</div>
</div>
);
};
export default Form;

View File

@ -7,22 +7,22 @@
margin: 0 0 -5px 29px;
}
&__icon-question {
}
&__title {
font-weight: 700;
@include adaptiv-value("font-size", 28, 22, 1);
line-height: 79%;
color: #1458dd;
}
&__body {
position: relative;
z-index: 2;
display: block;
&:not(:last-child) {
margin: 0 0 13px 0;
}
p {
word-break: break-word;
background: #ffffff;

View File

@ -1,15 +0,0 @@
import React from "react";
import {LogoutButton} from "../LogoutButton/LogoutButton";
import './header.scss'
export const Header = () => {
return (
<div className='container header'>
<h2>
<span>Аутстаффинг</span> it-персонала
</h2>
<LogoutButton/>
</div>
)
};

View File

@ -0,0 +1,16 @@
import React from "react";
import { LogoutButton } from "../LogoutButton/LogoutButton";
import "./header.scss";
export const Header = () => {
return (
<div className="container header">
<h2>
<span>Аутстаффинг</span> it-персонала
</h2>
<LogoutButton />
</div>
);
};

View File

@ -9,7 +9,7 @@
flex: 1;
text-align: center;
color: #52b709;
font-family: 'GT Eesti Pro Display', sans-serif;
font-family: "GT Eesti Pro Display", sans-serif;
font-size: 5em;
font-weight: 700;
font-style: normal;

View File

@ -1,12 +0,0 @@
import SVGLoader from 'react-loader-spinner'
import './loader.scss'
import React from "react";
export const Loader = ({width = 50, height = 50, style}) => {
return (
<div className='loader'>
<SVGLoader type='Circles' color={style ? style : `#fff`} height={height} width={width}/>
</div>
)
};

View File

@ -0,0 +1,17 @@
import React from "react";
import SVGLoader from "react-loader-spinner";
import "./loader.scss";
export const Loader = ({ width = 50, height = 50, style }) => {
return (
<div className="loader">
<SVGLoader
type="Circles"
color={style ? style : `#fff`}
height={height}
width={width}
/>
</div>
);
};

View File

@ -5,6 +5,7 @@
justify-content: center;
align-items: center;
position: relative;
&:hover {
path {
fill: #6aaf5c;

View File

@ -1,33 +0,0 @@
import React, {useState} from 'react'
import {useNavigate} from 'react-router-dom'
import {useSelector} from 'react-redux'
import {useLogout} from "../../hooks/useLogout";
import {Loader} from '../Loader/Loader'
import {getRole} from '../../redux/roleSlice'
import './logoutButton.scss'
export const LogoutButton = () => {
const [isLoggingOut, setIsLoggingOut] = useState(false);
const userRole = useSelector(getRole);
const navigate = useNavigate();
const {logout} = useLogout();
return (
<button
className='logout-button'
onClick={() => {
setIsLoggingOut(true);
logout();
setIsLoggingOut(false);
navigate(userRole === 'ROLE_DEV' ? '/authdev' : '/auth')
}}
>
{isLoggingOut ? <Loader/> : 'Выйти'}
</button>
)
};

View File

@ -0,0 +1,31 @@
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useLogout } from "../../hooks/useLogout";
import { Loader } from "../Loader/Loader";
import { getRole } from "../../redux/roleSlice";
import "./logoutButton.scss";
export const LogoutButton = () => {
const [isLoggingOut, setIsLoggingOut] = useState(false);
const userRole = useSelector(getRole);
const navigate = useNavigate();
const { logout } = useLogout();
return (
<button
className="logout-button"
onClick={() => {
setIsLoggingOut(true);
logout();
setIsLoggingOut(false);
navigate(userRole === "ROLE_DEV" ? "/authdev" : "/auth");
}}
>
{isLoggingOut ? <Loader /> : "Выйти"}
</button>
);
};

View File

@ -1,5 +1,4 @@
.logout-button {
position: relative;
z-index: 100;
display: flex;
@ -14,7 +13,7 @@
background-color: #6aaf5c;
color: #ffffff;
border: 3px solid #6aaf5c;
font-family: 'Muller', sans-serif;
font-family: "Muller", sans-serif;
text-align: center;
&:hover {

View File

@ -3,7 +3,6 @@ import { useSelector, useDispatch } from "react-redux";
import OutstaffingBlock from "../OutstaffingBlock/OutstaffingBlock";
import TagSelect from "../Select/TagSelect";
import {
selectTags,
getPositionId,

View File

@ -1,118 +0,0 @@
import React from 'react'
import OutsideClickHandler from 'react-outside-click-handler'
import {useDispatch, useSelector} from 'react-redux'
import {
selectItems,
selectedItems,
profiles,
} from '../../redux/outstaffingSlice'
import {apiRequest} from "../../api/request";
import './outstaffingBlock.scss'
const handlePositionClick = (
{
dispatch,
positionId,
isSelected,
onSelect,
apiRequest
}) => {
if (isSelected) {
apiRequest('/profile', {
params: {
limit: 1000
},
}).then((profileArr) => {
dispatch(profiles(profileArr));
dispatch(selectedItems([]));
onSelect(positionId)
})
} else {
apiRequest('/profile', {
params: {
limit: '1000',
position_id: positionId
},
}).then((res) => {
dispatch(profiles(res));
dispatch(selectedItems([]));
onSelect(positionId)
})
}
};
const OutstaffingBlock = (
{
dataTags = [],
selected,
img,
header,
positionId,
isSelected,
onSelect
}) => {
const dispatch = useDispatch();
const itemsArr = useSelector(selectItems);
const handleBlockClick = (item, id) => {
if (!itemsArr.find((el) => item === el.value)) {
dispatch(selectedItems([...itemsArr, {id, value: item, label: item}]))
}
};
let classes;
dataTags.forEach((el) => {
if (el.name === 'skills_back') {
classes = 'back'
} else if (el.name === 'skills_design') {
classes = 'des'
} else if (el.name === 'skills_front') {
classes = 'front'
}
});
return (
<OutsideClickHandler onOutsideClick={() => isSelected && onSelect(null)}>
<div className={`outstaffing-block${isSelected ? ' outstaffing-block__selected' : ''}`}>
<div className={`outstaffing-block__img ${selected ? ' outstaffing-block__border' : ''}`}
onClick={() => handlePositionClick(
{
dispatch,
positionId,
isSelected,
onSelect,
apiRequest
})
}>
<h3>{header}</h3>
<img className={classes} src={img} alt='img'/>
</div>
<div className={`${selected ? 'outstaffing-block__mobile--block' : 'outstaffing-block__mobile--none'}`} >
<p className='outstaffing-block__text'># Популярный стек</p>
{dataTags && (
<ul className='outstaffing-block__items'>
{dataTags.map((item) => (
<li
key={item.id}
onClick={() => handleBlockClick(item.value, item.id)}
>
{item.value}
</li>
))}
</ul>
)}
</div>
</div>
</OutsideClickHandler>
)
};
export default OutstaffingBlock

View File

@ -0,0 +1,126 @@
import React from "react";
import OutsideClickHandler from "react-outside-click-handler";
import { useDispatch, useSelector } from "react-redux";
import {
selectItems,
selectedItems,
profiles,
} from "../../redux/outstaffingSlice";
import { apiRequest } from "../../api/request";
import "./outstaffingBlock.scss";
const handlePositionClick = ({
dispatch,
positionId,
isSelected,
onSelect,
apiRequest,
}) => {
if (isSelected) {
apiRequest("/profile", {
params: {
limit: 1000,
},
}).then((profileArr) => {
dispatch(profiles(profileArr));
dispatch(selectedItems([]));
onSelect(positionId);
});
} else {
apiRequest("/profile", {
params: {
limit: "1000",
position_id: positionId,
},
}).then((res) => {
dispatch(profiles(res));
dispatch(selectedItems([]));
onSelect(positionId);
});
}
};
const OutstaffingBlock = ({
dataTags = [],
selected,
img,
header,
positionId,
isSelected,
onSelect,
}) => {
const dispatch = useDispatch();
const itemsArr = useSelector(selectItems);
const handleBlockClick = (item, id) => {
if (!itemsArr.find((el) => item === el.value)) {
dispatch(selectedItems([...itemsArr, { id, value: item, label: item }]));
}
};
let classes;
dataTags.forEach((el) => {
if (el.name === "skills_back") {
classes = "back";
} else if (el.name === "skills_design") {
classes = "des";
} else if (el.name === "skills_front") {
classes = "front";
}
});
return (
<OutsideClickHandler onOutsideClick={() => isSelected && onSelect(null)}>
<div
className={`outstaffing-block${
isSelected ? " outstaffing-block__selected" : ""
}`}
>
<div
className={`outstaffing-block__img ${
selected ? " outstaffing-block__border" : ""
}`}
onClick={() =>
handlePositionClick({
dispatch,
positionId,
isSelected,
onSelect,
apiRequest,
})
}
>
<h3>{header}</h3>
<img className={classes} src={img} alt="img" />
</div>
<div
className={`${
selected
? "outstaffing-block__mobile--block"
: "outstaffing-block__mobile--none"
}`}
>
<p className="outstaffing-block__text"># Популярный стек</p>
{dataTags && (
<ul className="outstaffing-block__items">
{dataTags.map((item) => (
<li
key={item.id}
onClick={() => handleBlockClick(item.value, item.id)}
>
{item.value}
</li>
))}
</ul>
)}
</div>
</div>
</OutsideClickHandler>
);
};
export default OutstaffingBlock;

View File

@ -1,15 +1,15 @@
body {
font-family: 'LabGrotesque', sans-serif !important;
font-family: "LabGrotesque", sans-serif !important;
}
.container {
max-width: 1160px !important;
}
.catalog {
background: #F1F1F1;
background: #f1f1f1;
height: 100%;
min-height: 100vh;
font-family: 'LabGrotesque', sans-serif;
font-family: "LabGrotesque", sans-serif;
padding-top: 23px;
&__title {
@ -37,7 +37,7 @@ body {
}
& > p {
font-family: 'GT Eesti Pro Display';
font-family: "GT Eesti Pro Display";
font-size: 1.2em;
font-weight: 300;
font-style: normal;
@ -60,7 +60,7 @@ body {
right: 13%;
top: 30%;
max-width: 130px;
font-family: 'GT Eesti Pro Display';
font-family: "GT Eesti Pro Display";
font-size: 18px;
font-weight: 400;
font-style: normal;
@ -85,7 +85,7 @@ body {
&__items {
li {
font-family: 'GT Eesti Pro Display';
font-family: "GT Eesti Pro Display";
font-size: 1.8em;
font-weight: 100;
font-style: normal;

View File

@ -1,16 +0,0 @@
import React from 'react'
import {Link} from "react-router-dom";
import './profileBreadcrumbs.scss'
export const ProfileBreadcrumbs = ({ links }) => {
return (
<div className='profileBreadcrumbs'>
{links.map((link, index) => {
return <Link key={index} to={link.link}>{link.name}</Link>
})
}
</div>
)
}

View File

@ -0,0 +1,18 @@
import React from "react";
import { Link } from "react-router-dom";
import "./profileBreadcrumbs.scss";
export const ProfileBreadcrumbs = ({ links }) => {
return (
<div className="profileBreadcrumbs">
{links.map((link, index) => {
return (
<Link key={index} to={link.link}>
{link.name}
</Link>
);
})}
</div>
);
};

View File

@ -7,7 +7,7 @@
}
a {
color: #5B6871;
color: #5b6871;
font-weight: 400;
font-size: 12px;
line-height: 16px;
@ -31,7 +31,7 @@
}
&:after {
content: '';
content: "";
background-image: url("../../images/BreadcrumbsArrow.png");
background-repeat: no-repeat;
width: 7px;

View File

@ -1,20 +1,16 @@
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, Navigate } from "react-router-dom";
import { getReports } from "../Calendar/calendarHelper";
import { Link, Navigate } from "react-router-dom";
import moment from "moment";
import { ProfileCalendarComponent } from "./ProfileCalendarComponent";
import { Loader } from "../Loader/Loader";
import { ProfileHeader } from "../ProfileHeader/ProfileHeader";
import { ProfileBreadcrumbs } from "../../components/ProfileBreadcrumbs/ProfileBreadcrumbs";
import { ProfileBreadcrumbs } from "../ProfileBreadcrumbs/ProfileBreadcrumbs";
import { Footer } from "../Footer/Footer";
import { Navigation } from "../Navigation/Navigation";
import { ViewReport } from "../../pages/ViewReport/ViewReport";
import { urlForLocal } from "../../helper";
import { apiRequest } from "../../api/request";
import { getProfileInfo } from "../../redux/outstaffingSlice";
import {

View File

@ -1,23 +1,25 @@
import React, { useState, useEffect } from "react";
import arrow from "../../images/arrowCalendar.png";
import rectangle from "../../images/rectangle__calendar.png";
import calendarIcon from "../../images/calendar_icon.png";
import moment from "moment";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import {
setReportDate,
setRequestDate,
setSendRequest,
} from "../../redux/reportSlice";
import {
calendarHelper,
currentMonthAndDay,
getReports,
hourOfNum,
} from "../Calendar/calendarHelper";
import {
setReportDate,
setRequestDate,
setSendRequest,
} from "../../redux/reportSlice";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import ShortReport from "../ShortReport/ShortReport";
import arrow from "../../images/arrowCalendar.png";
import rectangle from "../../images/rectangle__calendar.png";
import calendarIcon from "../../images/calendar.svg";
import moment from "moment";
import "moment/locale/ru";
import "./../Calendar/calendarComponent.scss";

View File

@ -1,81 +1,100 @@
import React, {useState, useEffect} from 'react'
import {useSelector} from 'react-redux'
import {Link, Navigate, useNavigate} from 'react-router-dom'
import DatePicker, { registerLocale } from "react-datepicker"
import {getCorrectDate, getCreatedDate, hourOfNum} from '../Calendar/calendarHelper'
import ru from "date-fns/locale/ru"
import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { Link, Navigate, useNavigate } from "react-router-dom";
import DatePicker, { registerLocale } from "react-datepicker";
import {
getCorrectDate,
getCreatedDate,
hourOfNum,
} from "../Calendar/calendarHelper";
import ru from "date-fns/locale/ru";
registerLocale("ru", ru);
import {Loader} from '../Loader/Loader'
import { Loader } from "../Loader/Loader";
import { Footer } from "../Footer/Footer";
import { ProfileHeader } from "../ProfileHeader/ProfileHeader";
import {ProfileBreadcrumbs} from "../../components/ProfileBreadcrumbs/ProfileBreadcrumbs"
import { ProfileBreadcrumbs } from "../../components/ProfileBreadcrumbs/ProfileBreadcrumbs";
import { apiRequest } from "../../api/request";
import {getReportDate} from '../../redux/reportSlice'
import { getReportDate } from "../../redux/reportSlice";
import calendarIcon from '../../images/calendar_icon.png'
import ellipse from '../../images/ellipse.png'
import remove from '../../images/remove.png'
import calendarIcon from "../../images/calendar.svg";
import ellipse from "../../images/ellipse.png";
import remove from "../../images/remove.png";
import arrow from "../../images/right-arrow.png";
import './reportForm.scss'
import "./reportForm.scss";
import "react-datepicker/dist/react-datepicker.css";
import { Navigation } from '../Navigation/Navigation'
import { Navigation } from "../Navigation/Navigation";
const ReportForm = () => {
if(localStorage.getItem('role_status') === '18') {
return <Navigate to="/profile" replace/>
if (localStorage.getItem("role_status") === "18") {
return <Navigate to="/profile" replace />;
}
const navigate = useNavigate();
const reportDate = useSelector(getReportDate);
useEffect(() => {
initListeners()
}, [])
initListeners();
}, []);
const [isFetching, setIsFetching] = useState(false);
const [reportSuccess, setReportSuccess] = useState('');
const [startDate, setStartDate] = useState(reportDate ? new Date (reportDate._d) : new Date());
const [reportSuccess, setReportSuccess] = useState("");
const [startDate, setStartDate] = useState(
reportDate ? new Date(reportDate._d) : new Date()
);
const [datePickerOpen, setDatePickerOpen] = useState(false);
const [inputs, setInputs] = useState([{task: '', hours_spent: '', minutes_spent: 0}]);
const [troublesInputValue, setTroublesInputValue] = useState('');
const [scheduledInputValue, setScheduledInputValue] = useState('');
const [inputs, setInputs] = useState([
{ task: "", hours_spent: "", minutes_spent: 0 },
]);
const [troublesInputValue, setTroublesInputValue] = useState("");
const [scheduledInputValue, setScheduledInputValue] = useState("");
const addInput = () => {
setInputs((prev) => [...prev, {task: '', hours_spent: '', minutes_spent: 0}])
setInputs((prev) => [
...prev,
{ task: "", hours_spent: "", minutes_spent: 0 },
]);
};
const initListeners = () => {
document.addEventListener('click', closeByClickingOut)
}
document.addEventListener("click", closeByClickingOut);
};
const closeByClickingOut = (event) => {
const path = event.path || (event.composedPath && event.composedPath())
const path = event.path || (event.composedPath && event.composedPath());
if (event && !path.find((div) => div.classList && (div.classList.contains('report-form__block-img') || div.classList.contains('react-datepicker-popper')))) {
setDatePickerOpen(false)
}
if (
event &&
!path.find(
(div) =>
div.classList &&
(div.classList.contains("report-form__block-img") ||
div.classList.contains("react-datepicker-popper"))
)
) {
setDatePickerOpen(false);
}
};
const totalHours = inputs.reduce((a, b) => a + b.hours_spent, 0);
const deleteInput = (indexRemove) => {
setInputs((prev) => prev.filter((el, index) => index !== indexRemove))
setInputs((prev) => prev.filter((el, index) => index !== indexRemove));
};
const handler = () => {
for (let input of inputs) {
if (!input.task || !input.hours_spent) {
setReportSuccess('Заполните задачи');
setTimeout(() => setReportSuccess(''), 2000)
return
setReportSuccess("Заполните задачи");
setTimeout(() => setReportSuccess(""), 2000);
return;
}
}
apiRequest('/reports/create', {
method: 'POST',
apiRequest("/reports/create", {
method: "POST",
data: {
tasks: inputs,
difficulties: troublesInputValue,
@ -84,148 +103,215 @@ const ReportForm = () => {
status: 1,
},
}).then((res) => {
setReportSuccess('Отчет отправлен');
setReportSuccess("Отчет отправлен");
setTimeout(() => {
setReportSuccess('')
navigate('/profile/calendar');
}, 1000)
setReportSuccess("");
navigate("/profile/calendar");
}, 1000);
setInputs(() => []);
setTroublesInputValue('');
setScheduledInputValue('');
setTroublesInputValue("");
setScheduledInputValue("");
setIsFetching(false);
setInputs(() => [{task: '', hours_spent: '', minutes_spent: 0}]);
})
setInputs(() => [{ task: "", hours_spent: "", minutes_spent: 0 }]);
});
};
return (
<section className='report-form'>
<section className="report-form">
<ProfileHeader />
<Navigation />
<div className='container'>
<ProfileBreadcrumbs links={[{name: 'Главная', link: '/profile'},
{name: 'Ваша отчетность', link: '/profile/calendar'},
{name: 'Страница добавления нового отчета', link: '/report'}]}
<div className="container">
<ProfileBreadcrumbs
links={[
{ name: "Главная", link: "/profile" },
{ name: "Ваша отчетность", link: "/profile/calendar" },
{ name: "Страница добавления нового отчета", link: "/report" },
]}
/>
<h2 className='summary__title'>Ваши отчеты - <span>добавить отчет</span></h2>
<h2 className="summary__title">
Ваши отчеты - <span>добавить отчет</span>
</h2>
<div>
<div className='report__head'>
<Link className='calendar__back' to={`/profile/calendar`}>
<img src={arrow} alt=''/><p>Вернуться</p>
<div className="report__head">
<Link className="calendar__back" to={`/profile/calendar`}>
<img src={arrow} alt="" />
<p>Вернуться</p>
</Link>
</div>
</div>
<div className='report-form__content'>
<div className='report-form__block'>
<div className='report-form__block-title'>
<div className="report-form__content">
<div className="report-form__block">
<div className="report-form__block-title">
<h2>Добавление отчета за день</h2>
<h3>Дата заполнения отчета:</h3>
</div>
<div className='report-form__block-img' onClick={() => setDatePickerOpen(true)}>
<div
className="report-form__block-img"
onClick={() => setDatePickerOpen(true)}
>
<img
className='report-form__calendar-icon'
className="report-form__calendar-icon"
src={calendarIcon}
alt=''
alt=""
/>
{getCorrectDate(startDate)}
</div>
<DatePicker
className='datePicker'
className="datePicker"
open={datePickerOpen}
locale="ru"
selected={startDate}
onChange={(date) => {
setDatePickerOpen(false)
setStartDate(date)
setDatePickerOpen(false);
setStartDate(date);
}}
/>
<div className='report-form__task-list'>
<img src={ellipse} alt=''/>
<div className="report-form__task-list">
<img src={ellipse} alt="" />
<span>Какие задачи были выполнены?</span>
</div>
</div>
<div className='row'>
<div className='col-8'>
<div className='report-form__task-header'>
<p className='report-form__task-title--description'>
<div className="row">
<div className="col-8">
<div className="report-form__task-header">
<p className="report-form__task-title--description">
Краткое описание задачи
</p>
<p className='report-form__task-title--hours'>Количество часов</p>
<p className="report-form__task-title--hours">
Количество часов
</p>
</div>
{inputs.map((input, index) => {
return (
<form id={'input'} key={`input__${index}`} className='report-form__task-form'>
<div className='report-form__task-number'>
{index + 1}.
</div>
<div className='report-form__task-input report-form__task-input--description'>
<input value={inputs[index].task} className={!input.task && reportSuccess === 'Заполните задачи' ? 'checkTask' : ''} name='text' type='text'
onChange={e => setInputs(inputs.map((input, inputIndex) => {
<form
id={"input"}
key={`input__${index}`}
className="report-form__task-form"
>
<div className="report-form__task-number">{index + 1}.</div>
<div className="report-form__task-input report-form__task-input--description">
<input
value={inputs[index].task}
className={
!input.task && reportSuccess === "Заполните задачи"
? "checkTask"
: ""
}
name="text"
type="text"
onChange={(e) =>
setInputs(
inputs.map((input, inputIndex) => {
return index === inputIndex
? {
...input,
task: e.target.value
task: e.target.value,
}
: input
}))}/>
</div>
<div className='report-form__task-input report-form__task-input--hours'>
<input value={inputs[index].hours_spent} className={!input.hours_spent && reportSuccess === 'Заполните задачи' ? 'checkTask' : ''} name='number' type='number' min='1'
onChange={e => setInputs(inputs.map((input, inputIndex) => {
return index === inputIndex
? {
...input,
hours_spent: Number(e.target.value)
}
: input
}))}/>
</div>
{index > 0 &&
<div className='report-form__task-remove'>
<img onClick={() => deleteInput(index)} src={remove} alt=''/>
</div>
}
</form>
: input;
})
)
}
/>
</div>
<div className="report-form__task-input report-form__task-input--hours">
<input
value={inputs[index].hours_spent}
className={
!input.hours_spent &&
reportSuccess === "Заполните задачи"
? "checkTask"
: ""
}
name="number"
type="number"
min="1"
onChange={(e) =>
setInputs(
inputs.map((input, inputIndex) => {
return index === inputIndex
? {
...input,
hours_spent: Number(e.target.value),
}
: input;
})
)
}
/>
</div>
{index > 0 && (
<div className="report-form__task-remove">
<img
onClick={() => deleteInput(index)}
src={remove}
alt=""
/>
</div>
)}
</form>
);
})}
<div className='report-form__form-add'>
<p className='addMore' onClick={addInput}>+</p>
<div className="report-form__form-add">
<p className="addMore" onClick={addInput}>
+
</p>
<span>Добавить еще </span>
</div>
</div>
</div>
<div className='row'>
<div className='col-12'>
<div className='report-form__input-box'>
<div className='report-form__troubles'>
<img src={ellipse} alt=''/>
<div className="row">
<div className="col-12">
<div className="report-form__input-box">
<div className="report-form__troubles">
<img src={ellipse} alt="" />
<span>Какие сложности возникли?</span>
</div>
<input type='text' value={troublesInputValue} onChange={e => setTroublesInputValue(e.target.value)}/>
<div className='report-form__scheduled'>
<img src={ellipse} alt=''/>
<input
type="text"
value={troublesInputValue}
onChange={(e) => setTroublesInputValue(e.target.value)}
/>
<div className="report-form__scheduled">
<img src={ellipse} alt="" />
<span>Что планируется сделать завтра?</span>
</div>
<input type='text' value={scheduledInputValue} onChange={e => setScheduledInputValue(e.target.value)}/>
<input
type="text"
value={scheduledInputValue}
onChange={(e) => setScheduledInputValue(e.target.value)}
/>
</div>
</div>
</div>
<div className='row'>
<div className='col-12'>
<div className='report-form__footer'>
<button className='report-form__footer-btn' onClick={() => handler()}>
{isFetching ? <Loader/> : 'Отправить'}
<div className="row">
<div className="col-12">
<div className="report-form__footer">
<button
className="report-form__footer-btn"
onClick={() => handler()}
>
{isFetching ? <Loader /> : "Отправить"}
</button>
<p className='report-form__footer-text'>
Всего за день : <span>{totalHours} {hourOfNum(totalHours)}</span>
<p className="report-form__footer-text">
Всего за день :{" "}
<span>
{totalHours} {hourOfNum(totalHours)}
</span>
</p>
{reportSuccess &&
<p className={`report-form__footer-done ${reportSuccess === 'Заполните задачи' ? 'errorText' : ''}`}>{reportSuccess}</p>
}
{reportSuccess && (
<p
className={`report-form__footer-done ${
reportSuccess === "Заполните задачи" ? "errorText" : ""
}`}
>
{reportSuccess}
</p>
)}
</div>
</div>
</div>
@ -233,7 +319,7 @@ const ReportForm = () => {
</div>
<Footer />
</section>
)
);
};
export default ReportForm
export default ReportForm;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

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

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.278 23.9789L9.078 15.7389L4 20.2159V2.01887C3.99999 1.64616 4.10412 1.28087 4.30065 0.964186C4.49718 0.647505 4.77829 0.392041 5.11227 0.22661C5.44626 0.0611791 5.81982 -0.00763698 6.19083 0.0279244C6.56183 0.0634857 6.91552 0.20201 7.212 0.427871L21.117 12.4359L14.5 13.1699L18.645 21.2999L13.278 23.9789ZM9.684 12.5339L14.158 21.2999L15.947 20.4059L11.4 11.4999L16.338 10.9529L5.952 1.97987L5.994 15.7899L9.684 12.5339Z" fill="#374957"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@ -14,7 +14,7 @@ import { Footer } from "../../components/Footer/Footer";
import { apiRequest } from "../../api/request";
import cursorImg from "../../images/cursorImg.png";
import cursorImg from "../../images/cursorImg.svg";
import "./partnerRequests.scss";
import { Navigation } from "../../components/Navigation/Navigation";