feat: Добавление канбан доски
- просмотр/создание/изменение/удаление категорий; - просмотр/создание/изменение/удаление задач; - просмотр общей информации о канбан доске
This commit is contained in:
348
src/KBBoard.js
348
src/KBBoard.js
@@ -1,29 +1,355 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import Header from './Header';
|
||||
import './css/Board.css';
|
||||
|
||||
const KBBoard = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [user, setUser] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [info, setInfo] = useState({});
|
||||
const [categories, setCategories] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSession = async () => {
|
||||
const [crTask, setCrTask] = useState(false);
|
||||
const [crCateg, setCrCateg] = useState(false);
|
||||
const [edTask, setEdTask] = useState(false);
|
||||
const [edCateg, setEdCateg] = useState(false);
|
||||
const [delTask, setDelTask] = useState(false);
|
||||
const [delCateg, setDelCateg] = useState(false);
|
||||
|
||||
const [categoryTitle, setCategoryTitle] = useState('');
|
||||
const [categoryPosition, setCategoryPosition] = useState(null);
|
||||
const [taskTitle, setTaskTitle] = useState('');
|
||||
const [taskDescription, setTaskDescription] = useState('');
|
||||
const [taskPosition, setTaskPosition] = useState(null);
|
||||
const [taskCategory, setTaskCategory] = useState(null);
|
||||
const [taskCategori, setTaskCategori] = useState(null);
|
||||
const [editedTask, setEditedTask] = useState({});
|
||||
const [editedCateg, setEditedCateg] = useState({});
|
||||
|
||||
|
||||
|
||||
const loadBoardData = useCallback(async () => {
|
||||
try {
|
||||
setError('')
|
||||
const response = await axios.post('/api/boards/load', { id });
|
||||
} catch {}
|
||||
setInfo(response.data);
|
||||
setCategories(response.data.categories);
|
||||
} catch (err) {
|
||||
if (err.response?.data?.message === 'Token Error' || err.response?.data?.message === 'Invalid Token') {
|
||||
setError('Вы не авторизованы');
|
||||
setTimeout(() => {
|
||||
navigate('/login');
|
||||
}, 1000);
|
||||
} else {
|
||||
setError('Ошибка загрузки доски');
|
||||
}
|
||||
}
|
||||
}, [id, navigate]);
|
||||
useEffect(() => {
|
||||
if (id) loadBoardData();
|
||||
}, [id, loadBoardData]);
|
||||
|
||||
|
||||
const modalCrTask = (categori) => () => {
|
||||
setCrTask(!crTask);
|
||||
setTaskCategori(categori);
|
||||
setTaskTitle('');
|
||||
setTaskDescription('');
|
||||
}
|
||||
const modalCrCateg = () => {
|
||||
setCrCateg(!crCateg);
|
||||
setCategoryTitle('');
|
||||
}
|
||||
const modalEditTask = (task, id_categ) => () => {
|
||||
setEdTask(!edTask);
|
||||
setTaskCategori(id_categ);
|
||||
setEditedTask(task);
|
||||
setTaskTitle(task.title);
|
||||
setTaskDescription(task.description);
|
||||
setTaskPosition(task.position);
|
||||
setTaskCategory(task.category_id);
|
||||
}
|
||||
const modalEditCateg = (categ) => () => {
|
||||
setEdCateg(!edCateg);
|
||||
setEditedCateg(categ);
|
||||
setCategoryTitle(categ.title);
|
||||
setCategoryPosition(categ.position);
|
||||
}
|
||||
const modalDelTask = () => {
|
||||
setDelTask(!delTask);
|
||||
}
|
||||
const modalDelCateg = () => {
|
||||
setDelCateg(!delCateg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const createTask = async (e) => {
|
||||
e.preventDefault();
|
||||
const newTask = { category_id: taskCategori, title: taskTitle, description: taskDescription };
|
||||
axios.post('/api/boards/categories/tasks/create', newTask);
|
||||
await loadBoardData();
|
||||
modalCrTask(null)();
|
||||
};
|
||||
checkSession();
|
||||
}, [id]);
|
||||
const createCategory = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const newCategory = { board_id: id, title: categoryTitle};
|
||||
await axios.post('/api/boards/categories/create', newCategory);
|
||||
await loadBoardData();
|
||||
} catch (err) {
|
||||
setError(err.response.data.message)
|
||||
} finally {
|
||||
modalCrCateg();
|
||||
}
|
||||
};
|
||||
|
||||
const editTask = async (e) => {
|
||||
e.preventDefault();
|
||||
var newTask = { id: editedTask.id, update_method: "title", value: taskTitle };
|
||||
await axios.put('/api/boards/categories/tasks/update', newTask);
|
||||
newTask = { id: editedTask.id, update_method: "description", value: taskDescription };
|
||||
await axios.put('/api/boards/categories/tasks/update', newTask);
|
||||
newTask = { id: editedTask.id, update_method: "category", value: Number(taskCategory) };
|
||||
await axios.put('/api/boards/categories/tasks/update', newTask);
|
||||
await loadBoardData();
|
||||
modalEditTask({}, null)();
|
||||
};
|
||||
const editCategory = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const newCategory = { id: editedCateg.id, update_method: "title", value: categoryTitle};
|
||||
await axios.put('/api/boards/categories/update', newCategory);
|
||||
await loadBoardData();
|
||||
} catch (err) {
|
||||
setError(err.response.data.message)
|
||||
} finally {
|
||||
modalEditCateg({})();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const deleteCategory = async (e) => {
|
||||
e.preventDefault();
|
||||
await axios.delete('/api/boards/categories/delete', { data: {id: editedCateg.id} });
|
||||
await loadBoardData();
|
||||
modalDelCateg();
|
||||
modalEditCateg({})();
|
||||
}
|
||||
const deleteTask = async (e) => {
|
||||
e.preventDefault();
|
||||
await axios.delete('/api/boards/categories/tasks/delete', { data: { id: editedTask.id } });
|
||||
await loadBoardData();
|
||||
modalDelTask();
|
||||
modalEditTask({},null)();
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="app-container">
|
||||
<Header />
|
||||
<div className="">
|
||||
<div className="page-container">
|
||||
{
|
||||
error && <div className="error">{error}</div>
|
||||
}
|
||||
<div className="inf-panel" >
|
||||
<div className="row">
|
||||
<h3>{info.title}</h3>
|
||||
<p>
|
||||
<strong>Участники: </strong> В разработке
|
||||
</p>
|
||||
<p>
|
||||
<strong>Владелец: </strong> {" "+info.owner?.display_name}
|
||||
<img className="nav-avatar" src={info.owner?.avatar_url} alt=''></img>
|
||||
</p>
|
||||
</div>
|
||||
<div className="row">
|
||||
<p><strong>Описание: </strong> {info.description ? info.description : 'Отсутствует'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="set-panel" >
|
||||
<p>SetingPanel:В разработке</p>
|
||||
</div>
|
||||
<div className="board-panel" >
|
||||
{categories.map((category) => (
|
||||
<div className="categori" key={category.position}>
|
||||
<button onClick={modalEditCateg(category)}><h3>{category.title}</h3></button>
|
||||
<div className="categ-h">
|
||||
<p>Позиция: {category.position}</p>
|
||||
<p>Задач: {category.tasks.length}</p>
|
||||
</div>
|
||||
<div className='task create'>
|
||||
<button onClick={modalCrTask(category.id)}>
|
||||
Новая задача
|
||||
</button>
|
||||
</div>
|
||||
<div className='task-list'>
|
||||
{category.tasks.length > 0 ? (
|
||||
category.tasks.map((task) => (
|
||||
<button className='task' onClick={modalEditTask(task, category.id)} key={task.id}>
|
||||
<div>{task.title}</div>
|
||||
<div>{task.description}</div>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p>Нет задач</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{categories.length < 10 ? (
|
||||
<div className="categori create">
|
||||
<div className="bib">
|
||||
<button onClick={modalCrCateg}>
|
||||
<div>
|
||||
+
|
||||
</div>
|
||||
Новая категория
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{crCateg && (
|
||||
<div className="confirm-modal">
|
||||
<div className="modal-content">
|
||||
<div><h3>Новая категория</h3></div>
|
||||
<form onSubmit={createCategory}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={categoryTitle}
|
||||
onChange={(e) => setCategoryTitle(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Подтвердить</button>
|
||||
</form>
|
||||
<button onClick={modalCrCateg}>Отменить</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{crTask && (
|
||||
<div className="confirm-modal">
|
||||
<div className="modal-content">
|
||||
<div><h3>Новая задача</h3></div>
|
||||
<form onSubmit={createTask}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={taskTitle}
|
||||
onChange={(e) => setTaskTitle(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={taskDescription}
|
||||
onChange={(e) => setTaskDescription(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Подтвердить</button>
|
||||
</form>
|
||||
<button onClick={modalCrTask(null)}>Отменить</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{edCateg && (
|
||||
<div className="confirm-modal">
|
||||
<div className="modal-content">
|
||||
<div><h3>Изменение категории</h3></div>
|
||||
<form onSubmit={editCategory}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={categoryTitle}
|
||||
onChange={(e) => setCategoryTitle(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Подтвердить</button>
|
||||
</form>
|
||||
<button onClick={modalEditCateg({})}>Отменить</button>
|
||||
<button className="Important-button" onClick={modalDelCateg}>Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{edTask && (
|
||||
<div className="confirm-modal">
|
||||
<div className="modal-content">
|
||||
<div><h3>Изменение задачи</h3></div>
|
||||
<form onSubmit={editTask}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={taskTitle}
|
||||
onChange={(e) => setTaskTitle(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={taskDescription}
|
||||
onChange={(e) => setTaskDescription(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<label >Категория:</label>
|
||||
<select value={taskCategory} onChange={(e) => setTaskCategory(e.target.value)}>
|
||||
{categories.map((category) => (
|
||||
<option key={category.position} value={category.id}>
|
||||
{category.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Подтвердить</button>
|
||||
</form>
|
||||
<button onClick={modalEditTask({},null)}>Отменить</button>
|
||||
<button className="Important-button" onClick={modalDelTask}>Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{delTask && (
|
||||
<div className="confirm-modal">
|
||||
<div className="modal-content">
|
||||
<div><h3>Удаление задачи</h3></div>
|
||||
<form onSubmit={deleteTask}>
|
||||
<label >Вы точно хотите удалить задачу {editedTask.title}</label>
|
||||
<button onClick={modalDelTask}>Отменить</button>
|
||||
<button className="Important-button" type="submit">Подтвердить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{delCateg && (
|
||||
<div className="confirm-modal">
|
||||
<div className="modal-content">
|
||||
<div><h3>Удаление категории</h3></div>
|
||||
<form onSubmit={deleteCategory}>
|
||||
<label >Вы точно хотите удалить эту категорию</label>
|
||||
<button onClick={modalDelCateg}>Отменить</button>
|
||||
<button className="Important-button" type="submit">Подтвердить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
242
src/css/Board.css
Normal file
242
src/css/Board.css
Normal file
@@ -0,0 +1,242 @@
|
||||
.inf-panel{
|
||||
flex: 0 0 auto;
|
||||
background-color: #2b3245;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.inf-panel strong {
|
||||
margin-right: 1ch;
|
||||
}
|
||||
|
||||
.inf-panel .row{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row p, .row h3{
|
||||
display: flex;
|
||||
margin: 0px 0px;
|
||||
word-break: break-all;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.row+.row p {
|
||||
margin: 20px 0px 0px 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.set-panel{
|
||||
flex: 0 0 auto;
|
||||
background-color: #2b3245;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.board-panel{
|
||||
flex: 1 1 auto;
|
||||
background-color: #2b3245;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
gap: 15px;
|
||||
padding: 5px;
|
||||
border: 5px dashed #2b3245;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.categori{
|
||||
flex: 1 1 auto; /* занимает всё свободное место */
|
||||
overflow-y: auto; /* вертикальный скролл при переполнении */
|
||||
overflow-x: hidden; /* горизонтальный скролл отключён */
|
||||
border: 3px solid #3d4763;
|
||||
background-color: #3d4763;
|
||||
min-width: 300px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categori>button{
|
||||
display: flex;
|
||||
background-color: #0000;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
justify-content: center;
|
||||
}
|
||||
.categori>button:hover{
|
||||
background-color: #0000;
|
||||
}
|
||||
|
||||
.bib {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.categ-h{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
flex: 1 1 auto; /* занимает всё свободное место */
|
||||
overflow-y: auto; /* вертикальный скролл при переполнении */
|
||||
overflow-x: hidden; /* горизонтальный скролл отключён */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
min-width: 300px;
|
||||
margin-top: 16px;
|
||||
margin-right: -4px; /* компенсируем отступ контейнера */
|
||||
padding-right: 4px; /* создаём отступ для контента */
|
||||
}
|
||||
|
||||
.task {
|
||||
text-align: left;
|
||||
margin: 0px;
|
||||
|
||||
color: #CAD1D8;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
background-color: #2b3245;
|
||||
border: 2px solid #617099;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.task+.task {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.task:hover {
|
||||
background-color: #3d4763;
|
||||
}
|
||||
|
||||
.create {
|
||||
margin-top: 16px;
|
||||
background-color: #fff0;
|
||||
border: 3px dashed #3d4763;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow-y: hidden;
|
||||
button{
|
||||
height: 100%;
|
||||
background-color: #fff0;
|
||||
font-size: 20px;
|
||||
color: #3d4763;
|
||||
margin: 0px;
|
||||
div{
|
||||
font-size: 50px;
|
||||
}
|
||||
}
|
||||
button:hover{
|
||||
background-color: #3d4763;
|
||||
color: #CAD1D8;
|
||||
}
|
||||
}
|
||||
|
||||
.categori .create {
|
||||
margin-top: 0px;
|
||||
background-color: #fff0;
|
||||
border: 3px dashed #617099;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow-y: unset;
|
||||
button{
|
||||
height: 50px;
|
||||
background-color: #fff0;
|
||||
font-size: 20px;
|
||||
color: #617099;
|
||||
margin: 0px;
|
||||
div{
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
button:hover{
|
||||
background-color: #617099;
|
||||
color: #CAD1D8;
|
||||
}
|
||||
}
|
||||
|
||||
.categori.create {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.task.create {
|
||||
margin-top: 16px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Стили для полосы прокрутки (опционально) */
|
||||
.task-list::-webkit-scrollbar{
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.board-panel::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.task-list::-webkit-scrollbar-track ,
|
||||
.board-panel::-webkit-scrollbar-track {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.task-list::-webkit-scrollbar-thumb,
|
||||
.board-panel::-webkit-scrollbar-thumb {
|
||||
background: #aaa;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.task-list::-webkit-scrollbar-thumb:hover,
|
||||
.board-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: #888;
|
||||
}
|
||||
Reference in New Issue
Block a user