feat: Добавление канбан доски

- просмотр/создание/изменение/удаление категорий;
- просмотр/создание/изменение/удаление задач;
- просмотр общей информации о канбан доске
This commit is contained in:
Vladiysss
2026-02-22 21:26:36 +03:00
parent e900747406
commit a2ce9043e7
2 changed files with 582 additions and 14 deletions

View File

@@ -1,29 +1,355 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
import Header from './Header'; import Header from './Header';
import './css/Board.css';
const KBBoard = () => { const KBBoard = () => {
const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const [user, setUser] = useState(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [info, setInfo] = useState({});
const [categories, setCategories] = useState([]);
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 });
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(() => { useEffect(() => {
const checkSession = async () => { if (id) loadBoardData();
try { }, [id, loadBoardData]);
const response = await axios.post('/api/boards/load', { id });
} catch {}
}; const modalCrTask = (categori) => () => {
checkSession(); setCrTask(!crTask);
}, [id]); 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)();
};
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 ( return (
<> <div className="app-container">
<Header /> <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>
</> </div>
); );
} }

242
src/css/Board.css Normal file
View 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;
}