Merge branch 'main' into landing

# Conflicts:
#	src/pages/roles/GuestPage.jsx
This commit is contained in:
Mikola
2024-04-16 17:02:36 +03:00
15 changed files with 1481 additions and 1212 deletions

View File

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useFormValidation } from "@hooks/useFormValidation";
import { useNotification } from "@hooks/useNotification";
@ -17,6 +18,7 @@ import "./modalRegistration.scss";
export const ModalRegistration = ({ active, setActive }) => {
const [loader, setLoader] = useState(false);
const [isPartner, setIsPartner] = useState(false);
const navigate = useNavigate();
const fields = {
username: "",
@ -145,6 +147,7 @@ export const ModalRegistration = ({ active, setActive }) => {
onClick={async (e) => {
e.preventDefault();
await handleSubmit(e);
navigate("/welcome-page");
}}
styles="button-box__submit"
>

View File

@ -719,7 +719,6 @@
.button-add {
margin: 0 30px;
align-self: flex-end;
}
}

View File

@ -0,0 +1,193 @@
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { movePositionProjectTask } from "@redux/projectsTrackerSlice";
import { getCorrectDate } from "@utils/calendarHelper";
import { removeLast, urlForLocal } from "@utils/helper";
import TrackerSelectColumn from "@components/TrackerSelectColumn/TrackerSelectColumn";
import commentsBoard from "assets/icons/commentsBoard.svg";
import filesBoard from "assets/icons/filesBoard.svg";
import avatarMok from "assets/images/avatarMok.png";
import "./trackerCardTask.scss";
const TrackerCardTask = ({
task,
projectBoard,
titleColor,
column,
openTicket,
startWrapperIndexTest,
setWrapperHover
}) => {
const dispatch = useDispatch();
const [taskHover, setTaskHover] = useState({});
const priority = {
2: "Высокий",
1: "Средний",
0: "Низкий"
};
const priorityClass = {
2: "high",
1: "middle",
0: "low"
};
function dragDropTaskHandler(e, task, column) {
e.preventDefault();
if (task.id === startWrapperIndexTest.current.task.id) {
return;
}
const finishTask = column.tasks.indexOf(task);
dispatch(
movePositionProjectTask({
startTask: startWrapperIndexTest.current.task,
finishTask: task,
finishIndex: finishTask
})
);
}
function dragOverTaskHandler(e, task) {
e.preventDefault();
if (startWrapperIndexTest.current.task.id === task.id) {
return;
}
setTaskHover((prevState) => ({ [prevState]: false, [task.id]: true }));
}
function dragStartHandler(e, task, columnId) {
startWrapperIndexTest.current = { task: task, index: columnId };
}
function dragLeaveTaskHandler() {
setTaskHover((prevState) => ({ [prevState]: false }));
}
function dragEndTaskHandler() {
setTaskHover((prevState) => ({ [prevState]: false }));
setWrapperHover((prevState) => ({
[prevState]: false
}));
}
useEffect(() => {
const tasksHover = {};
const columnHover = {};
if (Object.keys(projectBoard).length) {
projectBoard.columns.forEach((column) => {
columnHover[column.id] = false;
column.tasks.forEach((task) => (tasksHover[task.id] = false));
});
}
setWrapperHover(columnHover);
setTaskHover(tasksHover);
}, [projectBoard]);
return (
<div
key={task.id}
className={`tasks__board__item ${
taskHover[task.id] ? "task__hover" : ""
}`}
draggable={true}
onDragStart={(e) => dragStartHandler(e, task, column.id)}
onDragOver={(e) => dragOverTaskHandler(e, task)}
onDragLeave={(e) => dragLeaveTaskHandler(e)}
onDragEnd={() => dragEndTaskHandler()}
onDrop={(e) => dragDropTaskHandler(e, task, column)}
onClick={(e) => openTicket(e, task)}
>
<div
className="tasks__board__item__title"
onClick={() => {
if (window.innerWidth < 985) {
window.location.replace(`/tracker/task/${task.id}`);
}
}}
>
<p className="task__board__item__title">{task.title}</p>
</div>
<p
dangerouslySetInnerHTML={{
__html: task.description
}}
className="tasks__board__item__description"
></p>
{Boolean(task.mark.length) && (
<div className="tasks__board__item__tags">
{task.mark.map((tag) => {
return (
<div
className="tag-item"
key={tag.id}
style={{ background: tag.color }}
>
<p>{tag.slug}</p>
</div>
);
})}
</div>
)}
<div className="tasks__board__item__container">
{typeof task.execution_priority === "number" && (
<div className="tasks__board__item__priority">
<p></p>
<span className={priorityClass[task.execution_priority]}>
{priority[task.execution_priority]}
</span>
</div>
)}
{task.dead_line && (
<div className="tasks__board__item__dead-line">
<p></p>
<span style={{ color: titleColor }}>
{getCorrectDate(task.dead_line)}
</span>
</div>
)}
</div>
<div className="tasks__board__item__info">
<div className="tasks__board__item__executor">
<img
src={
task.executor?.avatar
? urlForLocal(task.executor?.avatar)
: avatarMok
}
alt="avatar"
/>
<span>
{removeLast(task.executor?.fio) || "Исполнитель не назначен"}
</span>
</div>
<div className="tasks__board__item__info__tags">
<div className="tasks__board__item__info__more">
<img src={commentsBoard} alt="commentsImg" />
<span>{task.comment_count}</span>
</div>
<div className="tasks__board__item__info__more">
<img src={filesBoard} alt="filesImg" />
<span>{task.file_count}</span>
</div>
</div>
</div>
<TrackerSelectColumn
columns={projectBoard.columns.filter((item) => item.id !== column.id)}
currentColumn={column}
task={task}
/>
</div>
);
};
export default TrackerCardTask;

View File

@ -0,0 +1,365 @@
.tasks {
&__board {
background: #f5f7f9;
box-shadow: 0px 2px 5px rgba(60, 66, 87, 0.04),
0px 0px 0px 1px rgba(60, 66, 87, 0.08), 0px 1px 1px rgba(0, 0, 0, 0.06);
border-radius: 8px;
padding: 12px 10px 12px 8px;
width: 360px;
display: flex;
flex-direction: column;
row-gap: 10px;
height: fit-content;
position: relative;
transition: all 0.3s ease;
transform: scaleY(-1);
min-height: 815px;
@media (max-width: 900px) {
min-width: auto;
width: 100%;
max-width: none;
transform: scaleX(1);
}
.tasks-container {
display: flex;
flex-direction: column;
row-gap: 8px;
max-height: 750px;
overflow: auto;
padding: 5px;
&::-webkit-scrollbar {
width: 3px;
border-radius: 10px;
}
&::-webkit-scrollbar-thumb {
background: #cbd9f9;
border-radius: 20px;
}
&::-webkit-scrollbar-track {
background: #c5c0c6;
border-radius: 20px;
}
}
&__hover {
box-shadow: 0px 2px 10px #9cc480, 0px 0px 0px 1px rgba(60, 66, 87, 0.08),
0px 1px 1px rgba(0, 0, 0, 0.06);
}
.task__hover {
box-shadow: 0 0 5px gray;
}
&__item {
width: 328px;
padding: 6px 10px 8px 10px;
position: relative;
box-shadow: 0px 3px 2px -2px rgba(0, 0, 0, 0.06),
0px 5px 3px -2px rgba(0, 0, 0, 0.02);
border-radius: 6px;
background: #ffffff;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: 0.4s;
&:hover {
transform: scale(1.025);
transition: 0.3s;
}
@media (max-width: 900px) {
width: 100%;
max-height: none;
&:hover {
transform: none;
}
}
&__hide {
opacity: 0;
}
&__title {
display: flex;
justify-content: space-between;
position: relative;
p {
color: #1a1919;
font-weight: 500;
font-size: 16px;
line-height: 20px;
margin-bottom: 0;
max-height: 100px;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
}
span {
cursor: pointer;
display: flex;
border-radius: 6px;
align-items: center;
justify-content: center;
font-size: 20px;
padding-bottom: 10px;
width: 24px;
height: 24px;
border: 1px solid #dddddd;
}
}
&__description {
margin: 4px 0;
color: #5c6165;
font-weight: 400;
font-size: 14px;
line-height: 120%;
max-height: 100px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
&__container {
display: flex;
justify-content: space-between;
}
&__info {
display: flex;
column-gap: 10px;
align-items: center;
justify-content: space-between;
pointer-events: none;
margin-top: 5px;
&__tags {
display: flex;
column-gap: 5px;
}
&__more {
cursor: pointer;
display: flex;
align-items: center;
span {
font-weight: 500;
font-size: 12px;
line-height: 15px;
color: #6e7c87;
margin-left: 5px;
}
}
&__avatars {
position: relative;
img {
position: relative;
}
img:first-child {
right: -15px;
z-index: 2;
}
}
}
&__priority {
display: flex;
align-items: center;
column-gap: 5px;
margin-top: 3px;
p {
font-weight: 500;
font-size: 14px;
}
span {
font-weight: 500;
font-size: 14px;
}
.high {
color: red;
}
.middle {
color: #cece00;
}
.low {
color: green;
}
}
&__dead-line {
display: flex;
align-items: center;
column-gap: 5px;
p {
font-weight: 500;
font-size: 14px;
color: #1458dd;
}
span {
font-weight: 500;
font-size: 14px;
}
img {
margin-top: -2px;
width: 18px;
}
}
&__executor {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
column-gap: 5px;
span {
max-width: 210px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
img {
width: 25px;
height: 25px;
}
}
&__tags {
display: flex;
flex-wrap: wrap;
column-gap: 6px;
row-gap: 3px;
margin: 3px 0;
.tag-item {
padding: 3px 10px;
border-radius: 10px;
color: white;
font-size: 12px;
}
}
}
.open-items {
cursor: pointer;
border-radius: 44px;
width: 33px;
height: 33px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: -15px;
font-size: 20px;
left: 165px;
color: white;
}
.more-items {
background: #8bcc60;
}
.less-items {
background: #f92828;
}
&__more {
padding-bottom: 50px;
}
.column__select {
position: absolute;
padding: 15px;
background: #e1fccf;
border-radius: 12px;
right: -20px;
top: 5px;
z-index: 7;
row-gap: 10px;
display: flex;
flex-direction: column;
@media (max-width: 910px) {
right: 10px;
top: 40px;
}
&__item {
cursor: pointer;
display: flex;
align-content: center;
img {
margin-right: 5px;
}
span {
font-size: 14px;
}
}
}
&__no-items {
font-weight: 500;
font-size: 25px;
transform: scaleY(-1);
@media (max-width: 900px) {
transform: none;
}
}
&__no-tasks {
display: flex;
flex-direction: column;
transform: scaleY(-1);
&-info {
display: flex;
align-items: center;
margin-bottom: 15px;
img {
width: 27px;
height: 27px;
margin-right: 5px;
}
p {
font-weight: 700;
font-size: 22px;
line-height: 32px;
}
}
&-more {
font-size: 14px;
line-height: 22px;
}
@media (max-width: 900px) {
transform: none;
}
}
}
}

View File

@ -0,0 +1,113 @@
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { filteredExecutorTasks } from "@redux/projectsTrackerSlice";
import { removeLast, urlForLocal } from "@utils/helper";
import arrowDown from "assets/icons/arrows/selectArrow.png";
import close from "assets/icons/close.png";
import avatarMok from "assets/images/avatarMok.png";
import "./trackerSelectExecutor.scss";
const TrackerSelectExecutor = ({
selectedExecutor,
setSelectedExecutor,
deleteSelectedExecutor,
projectBoard
}) => {
const [selectExecutorOpen, setSelectedExecutorOpen] = useState(false);
const dispatch = useDispatch();
const initListeners = () => {
document.addEventListener("click", closeByClickingOut);
};
const closeByClickingOut = (event) => {
const path = event.path || (event.composedPath && event.composedPath());
if (
event &&
!path.find(
(div) =>
div.classList &&
(div.classList.contains("tasks__head__executor") ||
div.classList.contains("tasks__head__executor-dropdown"))
)
) {
setSelectedExecutorOpen(false);
}
};
function executorFilter(user) {
dispatch(filteredExecutorTasks(user.user_id));
setSelectedExecutor(user);
}
useEffect(() => {
initListeners();
}, []);
if (selectedExecutor) {
return (
<div className="tasks__head__executor-selected">
<p>{removeLast(selectedExecutor.user.fio)}</p>
<img
className="avatar"
src={
selectedExecutor.user?.avatar
? urlForLocal(selectedExecutor.user.avatar)
: avatarMok
}
alt="avatar"
/>
<img
className="delete"
src={close}
alt="delete"
onClick={deleteSelectedExecutor}
/>
</div>
);
} else {
return (
<div
className="tasks__head__executor"
onClick={() => setSelectedExecutorOpen(!selectExecutorOpen)}
>
<p>Выберите исполнителя</p>
<img
className={selectExecutorOpen ? "open" : ""}
src={arrowDown}
alt="arrow"
/>
{selectExecutorOpen && (
<div className="tasks__head__executor-dropdown">
{projectBoard.projectUsers.map((user) => {
return (
<div
className="executor-dropdown__person"
key={user.user_id}
onClick={() => executorFilter(user)}
>
<p>{removeLast(user.user?.fio)}</p>
<img
src={
user.user?.avatar
? urlForLocal(user.user.avatar)
: avatarMok
}
alt="avatar"
/>
</div>
);
})}
</div>
)}
</div>
);
}
};
export default TrackerSelectExecutor;

View File

@ -0,0 +1,129 @@
.tasks {
&__head {
&__executor {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
margin-right: 10px;
border-radius: 8px;
border: 1px solid #e3e2e2;
padding: 2px 6px;
position: relative;
max-width: 190px;
width: 100%;
@media (max-width: 915px) {
margin-right: 0;
width: 100%;
max-width: none;
}
@media (max-width: 650px) {
border-color: gray;
}
&-selected {
display: flex;
align-items: center;
border-radius: 8px;
max-width: 220px;
width: 100%;
margin-right: 10px;
justify-content: center;
p {
color: #252c32;
font-weight: 400;
font-size: 14px;
line-height: 24px;
max-width: 155px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.avatar {
margin: 0 5px;
}
.delete {
cursor: pointer;
}
img {
display: flex;
width: 20px;
height: 20px;
}
@media (max-width: 915px) {
width: 100%;
max-width: none;
justify-content: start;
p {
font-size: 16px;
max-width: none;
}
}
}
p {
color: #252c32;
font-weight: 400;
font-size: 14px;
line-height: 24px;
}
img {
transition: all 0.15s ease;
margin-left: 5px;
}
.open {
transform: rotate(180deg);
}
&-dropdown {
position: absolute;
top: 33px;
left: 0;
background: white;
border-radius: 8px;
z-index: 5;
padding: 10px 10px;
display: flex;
flex-direction: column;
row-gap: 7px;
width: 100%;
.executor-dropdown__person {
display: flex;
justify-content: space-between;
align-items: center;
p {
max-width: 155px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@media (max-width: 915px) {
max-width: none;
}
}
img {
width: 20px;
height: 20px;
}
&:hover {
p {
font-weight: 600;
}
}
}
}
}
}
}

View File

@ -0,0 +1,275 @@
import React, { useEffect, useState } from "react";
import { HexColorPicker } from "react-colorful";
import { useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
import {
addNewTagToProject,
deleteTagProject,
setProjectBoardFetch
} from "@redux/projectsTrackerSlice";
import { apiRequest } from "@api/request";
import { useNotification } from "@hooks/useNotification";
import arrowDown from "assets/icons/arrows/selectArrow.png";
import close from "assets/icons/close.png";
import edit from "assets/icons/edit.svg";
import "./trackerTagList.scss";
const TrackerTagList = ({ projectBoard }) => {
const dispatch = useDispatch();
const projectId = useParams();
const { showNotification } = useNotification();
const [tagInfo, setTagInfo] = useState({ description: "", name: "" });
const [color, setColor] = useState("#aabbcc");
const [tags, setTags] = useState({
open: false,
add: false,
edit: false
});
function deleteTag(tagId) {
apiRequest("/mark/detach", {
method: "DELETE",
data: {
mark_id: tagId,
entity_type: 1,
entity_id: projectId.id
}
}).then(() => {
dispatch(deleteTagProject(tagId));
showNotification({
show: true,
text: "Тег удален",
type: "success"
});
});
}
function addNewTag() {
apiRequest("/mark/create", {
method: "POST",
data: {
title: tagInfo.description,
slug: tagInfo.name,
color: color,
status: 1
}
}).then((data) => {
apiRequest("/mark/attach", {
method: "POST",
data: {
mark_id: data.id,
entity_type: 1,
entity_id: projectId.id
}
}).then((data) => {
dispatch(addNewTagToProject(data.mark));
setTags((prevState) => ({
...prevState,
add: false
}));
showNotification({
show: true,
text: "Тег успешно создан",
type: "success"
});
});
});
}
function editTag() {
apiRequest("/mark/update", {
method: "PUT",
data: {
mark_id: tagInfo.editMarkId,
title: tagInfo.description,
slug: tagInfo.name,
color: color
}
}).then(() => {
dispatch(setProjectBoardFetch(projectId.id));
setTags((prevState) => ({
...prevState,
edit: false
}));
setTagInfo({ description: "", name: "" });
setColor("#aabbcc");
showNotification({
show: true,
text: "Тег успешно изменён",
type: "success"
});
});
}
const initListeners = () => {
document.addEventListener("click", closeByClickingOut);
};
const closeByClickingOut = (event) => {
const path = event.path || (event.composedPath && event.composedPath());
if (
event &&
!path.find(
(div) =>
div.classList &&
(div.classList.contains("tasks__head__tags") ||
div.classList.contains("tags__list"))
)
) {
setTags({ open: false, add: false, edit: false });
setTagInfo({ description: "", name: "" });
setColor("#aabbcc");
}
};
useEffect(() => {
initListeners();
}, []);
return (
<div className="tasks__head__tags">
<div
className="tags__add"
onClick={() => {
setTags((prevState) => ({
...prevState,
open: !tags.open
}));
}}
>
<p>Список тегов</p>
<img className={tags.open ? "open" : ""} src={arrowDown} alt="arrow" />
</div>
{tags.open && (
<div className="tags__list">
{!tags.add && !tags.edit && (
<div
className="add-new-tag"
onClick={() =>
setTags((prevState) => ({
...prevState,
add: true
}))
}
>
<p>Добавить новый тег</p>
<span>+</span>
</div>
)}
{!tags.add && !tags.edit && (
<div className="tags__list__created">
{projectBoard.mark.map((tag) => {
return (
<div className="tag-item" key={tag.id}>
<div className="tag-item__info">
<span
className="tag-item__color"
style={{ background: tag.color }}
/>
<div>
<span className="tag-item__info__name">{tag.slug}</span>
<p className="tag-item__description">{tag.title}</p>
</div>
</div>
<div className="tag-item__images">
<img
src={edit}
alt="edit"
onClick={() => {
setTags((prevState) => ({
...prevState,
edit: true
}));
setTagInfo({
description: tag.title,
name: tag.slug,
editMarkId: tag.id
});
setColor(tag.color);
}}
/>
<img
onClick={() => deleteTag(tag.id)}
className="delete"
src={close}
alt="delete"
/>
</div>
</div>
);
})}
</div>
)}
{(tags.add || tags.edit) && (
<div className="form-tag">
<input
className="form-tag__input"
placeholder="Описание метки"
maxLength="25"
value={tagInfo.description}
onChange={(e) =>
setTagInfo((prevState) => ({
...prevState,
description: e.target.value
}))
}
/>
<input
className="form-tag__input"
placeholder="Тег"
value={tagInfo.name}
maxLength="10"
onChange={(e) =>
setTagInfo((prevState) => ({
...prevState,
name: e.target.value
}))
}
/>
<HexColorPicker color={color} onChange={setColor} />
<button
onClick={() => {
tags.add ? addNewTag() : editTag();
}}
className={
tagInfo.name && tagInfo.description
? "form-tag__btn"
: "form-tag__btn disable"
}
>
{tags.add ? "Добавить" : "Изменить"}
</button>
{(tags.add || tags.edit) && (
<button
className={"form-tag__btn"}
onClick={() => {
setTags((prevState) => ({
...prevState,
add: false,
edit: false
}));
setTagInfo({
description: "",
name: ""
});
setColor("#aabbcc");
}}
>
Отмена
</button>
)}
</div>
)}
</div>
)}
</div>
);
};
export default TrackerTagList;

View File

@ -0,0 +1,208 @@
.tasks {
&__head {
&__tags {
position: relative;
img {
transition: all 0.15s ease;
margin-left: 5px;
}
.open {
transform: rotate(180deg);
}
.tags {
&__add {
display: flex;
align-items: center;
margin: 0 10px;
column-gap: 5px;
cursor: pointer;
padding: 5px;
border-radius: 8px;
border: 1px solid #e3e2e2;
max-height: 30px;
p {
white-space: nowrap;
font-weight: 400;
font-size: 14px;
line-height: 17px;
}
span {
width: 14px;
height: 14px;
font-weight: 400;
line-height: 16px;
border-radius: 50px;
align-items: center;
justify-content: center;
display: flex;
background: #99b4f3;
color: white;
font-size: 14px;
transition: all 0.15s ease;
}
}
&__list {
position: absolute;
border-radius: 8px;
background: #d9d9d9;
z-index: 8;
top: 30px;
left: -35px;
width: 216px;
display: flex;
flex-direction: column;
@media (max-width: 900px) {
left: 0px;
}
.close {
cursor: pointer;
width: 20px;
height: 20px;
position: absolute;
right: 10px;
top: 2px;
}
&__created {
display: flex;
flex-direction: column;
row-gap: 8px;
margin-top: 8px;
padding: 0 8px 8px;
.tag-item {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
column-gap: 5px;
padding: 0px 8px;
border-radius: 8px;
height: 40px;
max-height: 40px;
background: #fff;
&__description {
font-size: 12px;
word-break: break-word;
max-width: 115px;
max-height: 40px;
overflow: hidden;
text-wrap: wrap;
text-overflow: ellipsis;
}
&__color {
width: 22.25px;
height: 23.217px;
border-radius: 8px;
}
&__images {
display: flex;
flex-direction: column-reverse;
row-gap: 3px;
img {
height: 14px;
width: 14px;
cursor: pointer;
}
.delete {
width: 16px;
height: 16px;
}
}
&__info {
display: flex;
align-items: center;
column-gap: 10px;
&__name {
font-size: 12px;
font-weight: 600;
}
}
}
}
.add-new-tag {
display: flex;
align-items: center;
column-gap: 15px;
border-radius: 8px;
background: white;
color: #252c32;
height: 40px;
cursor: pointer;
justify-content: center;
margin: 8px 8px 0px;
p {
font-size: 15px;
}
span {
width: 19px;
height: 19px;
border-radius: 50px;
align-items: center;
justify-content: center;
display: flex;
background: #52b709;
color: white;
font-size: 16px;
transition: all 0.15s ease;
}
}
.form-tag {
display: flex;
flex-direction: column;
padding: 8px;
row-gap: 8px;
.arrow {
position: absolute;
cursor: pointer;
top: 5px;
width: 15px;
height: 15px;
transform: rotate(180deg);
}
&__input {
outline: none;
border-radius: 8px;
border: 1px solid #e3e2e2;
font-size: 15px;
padding: 5px;
}
&__btn {
outline: none;
border: none;
background: #252c32;
color: whitesmoke;
margin: 0 auto 0;
border-radius: 10px;
font-size: 15px;
padding: 5px 12px;
}
.disable {
opacity: 0.5;
pointer-events: none;
}
}
}
}
}
}
}