Merge pull request #36 from apuc/achievements

Achievements
This commit is contained in:
kavalar 2021-10-18 12:26:12 +03:00 committed by GitHub
commit 76c46067ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 474 additions and 62 deletions

View File

@ -1,24 +1,26 @@
import React, { Suspense, lazy } from 'react' import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { selectAuth } from './redux/outstaffingSlice'; import { selectAuth } from './redux/outstaffingSlice'
import 'bootstrap/dist/css/bootstrap.min.css' import 'bootstrap/dist/css/bootstrap.min.css'
import './fonts/stylesheet.css' import './fonts/stylesheet.css'
import { ProtectedRoute } from './components/ProtectedRoute/ProtectedRoute'; import { ProtectedRoute } from './components/ProtectedRoute/ProtectedRoute'
import { YMInitializer } from 'react-yandex-metrika'; import { YMInitializer } from 'react-yandex-metrika'
import AuthPageForDevelopers from './pages/AuthPageForDevelopers'; import AuthPageForDevelopers from './pages/AuthPageForDevelopers'
import AuthPageForPartners from './pages/AuthPageForPartners'; import AuthPageForPartners from './pages/AuthPageForPartners'
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage'
import CandidatePage from './pages/CandidatePage'; import CandidatePage from './pages/CandidatePage'
import CalendarPage from'./pages/CalendarPage'; import CalendarPage from './pages/CalendarPage'
import ReportPage from './pages/ReportFormPage.js'; import ReportPage from './pages/ReportFormPage.js'
import FormPage from './pages/FormPage.js'; import FormPage from './pages/FormPage.js'
import SingleReportPage from './pages/SingleReportPage'
const App = (props) => { const App = (props) => {
const isAuth = useSelector(selectAuth) const isAuth = useSelector(selectAuth)
return (<> return (
<>
<h1>IT Аутстаффинг в России</h1> <h1>IT Аутстаффинг в России</h1>
<Router> <Router>
<Switch> <Switch>
@ -29,10 +31,19 @@ const App = (props) => {
<AuthPageForPartners /> <AuthPageForPartners />
</Route> </Route>
<ProtectedRoute exact path='/' component={HomePage} /> <ProtectedRoute exact path='/' component={HomePage} />
<ProtectedRoute exact path='/candidate/:id' component={CandidatePage} /> <ProtectedRoute
exact
path='/candidate/:id'
component={CandidatePage}
/>
<ProtectedRoute path='/calendar' component={CalendarPage} /> <ProtectedRoute path='/calendar' component={CalendarPage} />
<ProtectedRoute exact path='/candidate/:id/form' component={FormPage} /> <ProtectedRoute
<ProtectedRoute path='/report' component={ReportPage} /> exact
path='/candidate/:id/form'
component={FormPage}
/>
<ProtectedRoute exact path='/report' component={ReportPage} />
<ProtectedRoute path='/report/:id' component={SingleReportPage} />
<ProtectedRoute component={() => <div>Page not found</div>} /> <ProtectedRoute component={() => <div>Page not found</div>} />
</Switch> </Switch>
</Router> </Router>
@ -51,5 +62,4 @@ const App = (props) => {
) )
} }
export default App export default App

View File

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

View File

@ -0,0 +1,51 @@
.achievement {
position: relative;
width: 50px;
height: 50px;
margin: 0 1rem;
&__popup {
position: absolute;
top: 50px;
left: 0;
width: 140px;
min-height: 70px;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
display: none;
text-align: left;
border-bottom: 3px solid #52b709;
border-left: 3px solid #52b709;
}
&__title {
color: #18586e;
font-size: 20px;
margin: 0;
padding: 0;
}
&__description {
color: #18586e;
font-size: 14px;
margin: 0;
padding: 0;
}
&__icon {
width: 50px;
height: 50px;
}
img {
width: 100%;
height: 100%;
}
&:hover .achievement__popup {
display: block;
}
}

View File

@ -35,7 +35,7 @@ const Description = ({ onLoadMore, isLoadingMore }) => {
{ {
candidatesListArr && candidatesListArr.length > 0 ? candidatesListArr.map((el) => ( candidatesListArr && candidatesListArr.length > 0 ? candidatesListArr.map((el) => (
<div className="row" key={el.id}> <div className="row" key={el.id}>
<div className="col-2"> <div className="col-2 col-xs-12">
<img className={style.description__img} src={el.photo} alt="" /> <img className={style.description__img} src={el.photo} alt="" />
</div> </div>
<div className="col-12 col-xl-6"> <div className="col-12 col-xl-6">

View File

@ -30,8 +30,11 @@
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.description__img { .description__img {
position: absolute; position: absolute;
top: 80px; top: 100px;
left: 0; left: calc(50% - 60px);
}
.description__wrapper {
padding: 45px 40px 0 40px;
} }
} }

View File

@ -3,6 +3,7 @@ import style from './Form.module.css';
import { fetchForm } from '../../server/server'; import { fetchForm } from '../../server/server';
import { auth } from '../../redux/outstaffingSlice'; import { auth } from '../../redux/outstaffingSlice';
import { useHistory, useParams, Redirect } from 'react-router-dom'; import { useHistory, useParams, Redirect } from 'react-router-dom';
import { Loader } from '../Loader/Loader';
import PhoneInput from 'react-phone-input-2' import PhoneInput from 'react-phone-input-2'
import 'react-phone-input-2/lib/style.css' import 'react-phone-input-2/lib/style.css'
import './form.css'; import './form.css';
@ -25,6 +26,7 @@ const Form = () => {
phone: '', phone: '',
comment: '', comment: '',
}); });
const [isFetching, setIsFetching] = useState(false);
const handleChange = (e) => { const handleChange = (e) => {
const { id, value } = e.target; const { id, value } = e.target;
@ -38,6 +40,7 @@ const Form = () => {
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
setIsFetching(true)
const formData = new FormData(); const formData = new FormData();
formData.append('profile_id', urlParams.id); formData.append('profile_id', urlParams.id);
formData.append('email', data.email); formData.append('email', data.email);
@ -48,14 +51,13 @@ const Form = () => {
profile_id: urlParams.id, profile_id: urlParams.id,
...data, ...data,
}, history, role, logout: dispatch(auth(false)) }).then( (res)=> res.json() }, history, role, logout: dispatch(auth(false)) }).then( (res)=> res.json()
.then( resJSON => setStatus(resJSON)) .then( resJSON => {
setStatus(resJSON);
setIsFetching(false);
})
) )
}; };
console.log('s',status)
return ( return (
<> <>
{status && <SweetAlert {status && <SweetAlert
@ -104,7 +106,7 @@ const Form = () => {
></textarea> ></textarea>
<button onClick={handleSubmit} className={style.form__btn} type="submit"> <button onClick={handleSubmit} className={style.form__btn} type="submit">
Отправить { isFetching ? <Loader /> : 'Отправить' }
</button> </button>
</form> </form>
</div> </div>

View File

@ -1,37 +1,53 @@
import React from 'react'; import React from 'react'
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom'
import maleBig from '../../images/medium_male_big.png'; import { Achievement } from '../Achievement/Achievement'
import style from './Sidebar.module.css';
import maleBig from '../../images/medium_male_big.png'
import './sidebar.scss'
const getYearsString = (years) => { const getYearsString = (years) => {
let yearsString; let yearsString
if (years % 10 === 1) { if (years % 10 === 1) {
yearsString = 'год'; yearsString = 'год'
} else if (years === 11 || years === 12 || years === 13 || years === 14) { } else if (years === 11 || years === 12 || years === 13 || years === 14) {
yearsString = 'лет'; yearsString = 'лет'
} else if (years % 10 === 2 || years % 10 === 3 || years % 10 === 4) { } else if (years % 10 === 2 || years % 10 === 3 || years % 10 === 4) {
yearsString = 'года'; yearsString = 'года'
} else { } else {
yearsString = 'лет'; yearsString = 'лет'
} }
return `${years} ${yearsString}`; return `${years} ${yearsString}`
} }
const Sidebar = ({ candidate }) => { const Sidebar = ({ candidate }) => {
console.log('c', candidate)
return ( return (
<div className={style.candidateSidebar}> <div className='candidateSidebar'>
<div className={style.candidateSidebar__info}> <div className='candidateSidebar__info'>
<img src={candidate.photo} alt="" /> <img src={candidate.photo} alt='' />
{ candidate && candidate.years_of_exp && <> {candidate && candidate.years_of_exp && (
<p className={style.candidateSidebar__info__e}>Опыт работы</p> <>
<p className={style.candidateSidebar__info__y}>{getYearsString(candidate.years_of_exp)}</p> <p className='candidateSidebar__experience-title'>Опыт работы</p>
</> } <p className='candidateSidebar__experience'>
{getYearsString(candidate.years_of_exp)}
</p>
</>
)}
<Link to={`/candidate/${candidate.id}/form`}> <Link to={`/candidate/${candidate.id}/form`}>
<button className={style.candidateSidebar__info__btn}>Выбрать к собеседованию</button> <button className='candidateSidebar__select'>
Выбрать к собеседованию
</button>
</Link> </Link>
<div className='candidateSidebar__achievements'>
{candidate &&
candidate.achievements &&
candidate.achievements.map((item) => {
return <Achievement achievement={item.achievement} />
})}
</div> </div>
</div> </div>
); </div>
}; )
}
export default Sidebar; export default Sidebar

View File

@ -6,6 +6,14 @@
border-bottom: none !important; border-bottom: none !important;
position: sticky; position: sticky;
top: 80px; top: 80px;
&__achievements {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
padding: 0 1rem;
margin-bottom: 80px;
}
} }
.candidateSidebar__info { .candidateSidebar__info {
@ -25,7 +33,7 @@
border-radius: 100px; border-radius: 100px;
} }
.candidateSidebar__info__e { .candidateSidebar__experience-title {
font-family: 'GT Eesti Pro Display'; font-family: 'GT Eesti Pro Display';
font-size: 1.8em; font-size: 1.8em;
font-weight: normal; font-weight: normal;
@ -35,7 +43,7 @@
margin-top: 20px; margin-top: 20px;
} }
.candidateSidebar__info__y { .candidateSidebar__experience {
font-family: 'GT Eesti Pro Display'; font-family: 'GT Eesti Pro Display';
font-size: 3em; font-size: 3em;
font-weight: 700; font-weight: 700;
@ -44,7 +52,7 @@
line-height: normal; line-height: normal;
} }
.candidateSidebar__info__btn { .candidateSidebar__select {
width: 280px; width: 280px;
height: 60px; height: 60px;
border-radius: 100px; border-radius: 100px;
@ -58,10 +66,10 @@
line-height: normal; line-height: normal;
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
margin-bottom: 120px; margin-bottom: 40px;
} }
.candidateSidebar__info__btn:hover { .candidateSidebar__select:hover {
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
color: #73c141; color: #73c141;
box-shadow: inset 0 0 0 3px #73c141; box-shadow: inset 0 0 0 3px #73c141;
@ -142,7 +150,8 @@
flex-direction: column; flex-direction: column;
} }
.candidateSidebar__info__e, .candidateSidebar__info__y { .candidateSidebar__info__e,
.candidateSidebar__info__y {
width: 180px; width: 180px;
} }
} }

View File

@ -0,0 +1,16 @@
import React from 'react'
import './taskItem.scss'
export const TaskItem = ({ index, text, hours }) => {
return (
<div className='task-item'>
<div className='task-item__index'>{index}.</div>
<div className='task-item__text'>{text}</div>
<div className='task-item__hours'>
<div className='task-item__hours-value'>{hours}</div>
<div className='task-item__hours-text'>Количество часов</div>
</div>
</div>
)
}

View File

@ -0,0 +1,72 @@
.task-item {
display: flex;
justify-content: flex-start;
align-items: center;
&__index {
margin-top: 0.2rem;
color: #282828;
font-family: 'GT Eesti Pro Display';
font-size: 20px;
font-weight: 700;
line-height: 48.74px;
text-align: left;
letter-spacing: 0.34px;
}
&__text {
min-width: 525px;
max-width: 525px;
margin-left: 1.6rem;
color: #000000;
font-family: 'GT Eesti Pro Display';
font-size: 15px;
font-weight: 400;
letter-spacing: normal;
line-height: normal;
text-align: left;
}
&__hours {
margin-left: 3.3rem;
display: flex;
&-value {
width: 34px;
height: 34px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 6px 5px 20px rgba(82, 151, 34, 0.21);
border-radius: 50%;
background-color: #ffffff;
background-image: linear-gradient(to top, #6aaf5c 0%, #52b709 100%),
linear-gradient(
36deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.16) 47%,
rgba(255, 255, 255, 0.17) 50%,
rgba(255, 255, 255, 0) 100%
);
color: #ffffff;
font-family: 'Muller Extra Bold';
font-size: 16px;
font-weight: 400;
text-align: left;
letter-spacing: 0.8px;
}
&-text {
margin-left: 1rem;
width: 69px;
height: 25px;
color: #000000;
font-family: 'GT Eesti Pro Display - Thin';
font-size: 13px;
font-weight: 400;
letter-spacing: normal;
line-height: normal;
text-align: left;
}
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5" height="9" viewBox="0 0 5 9"><g><g><image width="5" height="9" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAJCAYAAAD6reaeAAAAZ0lEQVQImV2OvRFAUBCE10ju56WvMUZIATSgDxogph96EGjAnOSZw4a7O98uoLRDucdHwiOEDEFaN80yKM8QMijVHlRVDuEVQhcCFx7EGKB8QPl8N5fULBNTpsRsnvUhrXfOUtr+P2/kAxfNQ+wYfgAAAABJRU5ErkJggg=="/></g></g></svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5" height="9" viewBox="0 0 5 9"><g><g><image width="5" height="9" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAJCAYAAAD6reaeAAAAaklEQVQImVWOvRFAQBSE10jez6XXGCOkABrQBw0Q0w89CDRgnuDO3Nlw55tvF8ijPELpTIWTHkIG4TkS1ELIoLzCrAAcVxB6ILyjacrPc0P5gvcuc3EdyS2RwdkFpyzBmdaHuD79bsafxwt/CxfNS2+70AAAAABJRU5ErkJggg=="/></g></g></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@ -25,3 +25,11 @@ h1 {
.container { .container {
position: relative !important; position: relative !important;
} }
@media (max-width: 568px) {
.col-xs-12 {
width: 100% !important;
max-width: 100%;
flex: initial;
}
}

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useParams, Link } from 'react-router-dom'; import { useHistory, useParams, Link } from 'react-router-dom';
import { currentCandidate, selectCurrentCandidate, auth } from '../redux/outstaffingSlice'; import { currentCandidate, selectCurrentCandidate, auth } from '../redux/outstaffingSlice';

View File

@ -0,0 +1,107 @@
import React from 'react'
import { WithLogout } from '../hoc/withLogout'
import arrowLeft from '../images/right-arrow.png'
import SVG from 'react-inlinesvg'
import prevDateArrowIcon from '../images/prevDateArrow.svg'
import nextDateArrowIcon from '../images/nextDateArrow.svg'
import './singleReportPage.scss'
import { TaskItem } from '../components/TaskItem/TaskItem'
const tasks = [
{
index: 1,
text: 'Задача «67 Навигационная система Главное меню Обновить иконки» заблокирована из-за отсутствия новых иконок',
hours: 3
},
{
index: 2,
text: 'Задача «83 Навигационная система Поиск по почтовому индексу Добавить экран поиска по почтовому индексу» не может быть завершена, т.к. работа над задачей «82 Навигационная система Разработать модуль поиска по почтовому индексу» ещё не начата',
hours: 3
}
]
const SingleReportPage = () => {
return (
<WithLogout>
<div className='single-report-page'>
<div className='single-report-page__back'>
<div className='single-report-page__back-arrow'>
<img src={arrowLeft} />
</div>
<div className='single-report-page__back-text'>
Вернуться к списку
</div>
</div>
<div className='single-report-page__title'>
<div className='single-report-page__title-text'>Отчет за день</div>
<div className='single-report-page__title-date'>
<div className='single-report-page__title-date--prev'>
<button>
<SVG src={prevDateArrowIcon} />
</button>
</div>
<div className='single-report-page__title-date--actual'>
<img src='' />
<p></p>
</div>
<div className='single-report-page__title-date--next single-report-page__title-date--enabled'>
<button>
<SVG src={nextDateArrowIcon} />
</button>
</div>
</div>
</div>
<div className='single-report-page__tasks'>
<div className='single-report-page__tasks-title'>
<div className='single-report-page__marker'></div>
<h3>Какие задачи были выполнены?</h3>
</div>
{tasks.map((task) => {
return (
<div className='single-report-page__tasks-item'>
<TaskItem {...task} />
</div>
)
})}
</div>
<div className='single-report-page__troubles'>
<div className='single-report-page__troubles-title'>
<div className='single-report-page__marker'></div>
<h3>Какие сложности возникли?</h3>
</div>
<div className='single-report-page__troubles-item'>
91 Навигационная система Поиск адреса Разобраться, почему
находятся несколько пересечений Невского пр. и Казанской ул.
</div>
</div>
<div className='single-report-page__scheduled'>
<div className='single-report-page__scheduled-title'>
<div className='single-report-page__marker'></div>
<h3>Что планируется сделать завтра?</h3>
</div>
<div className='single-report-page__scheduled-item'>
91 Навигационная система Поиск адреса Разобраться, почему
находятся несколько пересечений Невского пр. и Казанской ул.
</div>
</div>
<div className='single-report-page__footer'>
<div className='single-report-page__footer-rectangle'></div>
<div className='single-report-page__hours'>
<div className='single-report-page__hours-value'></div>
<div className='single-report-page__hours-text'></div>
</div>
</div>
</div>
</WithLogout>
)
}
export default SingleReportPage

View File

@ -0,0 +1,97 @@
.single-report-page {
padding-top: 4.6rem;
&__back {
display: flex;
justify-content: flex-start;
align-items: center;
&-text {
margin-left: 3.1rem;
color: #000000;
font-family: 'GT Eesti Pro Display - Thin';
font-size: 18px;
font-weight: 400;
letter-spacing: normal;
line-height: 36px;
text-align: left;
}
}
&__title {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 3rem;
&-text {
color: #282828;
font-family: 'GT Eesti Pro Display';
font-size: 33px;
font-weight: 700;
line-height: 48.74px;
text-align: left;
letter-spacing: 0.56px;
}
&-date {
margin-top: 0.2rem;
margin-left: 3rem;
display: flex;
justify-content: flex-start;
align-items: center;
}
button {
border: none;
outline: none;
width: 31px;
height: 31px;
background-color: #f6f6f6;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
}
&__marker {
width: 6px;
height: 6px;
background-color: #18586e;
border-radius: 50%;
margin-right: 0.8rem;
}
&__tasks,
&__troubles,
&__scheduled {
margin-top: 3.7rem;
&-title {
display: flex;
justify-content: flex-start;
align-items: center;
h3 {
color: #18586e;
font-family: 'GT Eesti Pro Display';
font-size: 20px;
font-weight: 500;
letter-spacing: normal;
line-height: normal;
text-align: left;
}
}
&-item {
margin-top: 2.4rem;
width: 580px;
color: #000000;
font-family: 'GT Eesti Pro Display';
font-size: 15px;
font-weight: 400;
letter-spacing: normal;
line-height: normal;
text-align: left;
}
}
}