О чем эта статья?

В этой статье вы узнаете, как создать доску Канбан так же, как в JIRA, Monday и Trello. Мы сделаем это с помощью красивой функции перетаскивания, используя React, Socket.io и React Beautiful DND. Пользователи смогут входить в систему, создавать и обновлять различные задачи, а также добавлять комментарии.

Novu — первая инфраструктура уведомлений с открытым исходным кодом

Просто краткая справка о нас. Novu — первая инфраструктура уведомлений с открытым исходным кодом. Мы в основном помогаем управлять всеми уведомлениями о продуктах. Это может быть In-App (значок колокольчика, как у вас в сообществе разработчиков — веб-сокеты), электронные письма, SMS и так далее.

Я был бы очень рад, если бы вы могли дать нам звезду! Это поможет мне делать больше статей каждую неделю 🚀
https://github.com/novuhq/novu

Мы также отправим классные подарки во время Хактоберфеста 😇

Что такое Socket.io?

Socket.io — это популярная библиотека JavaScript, которая позволяет нам создавать двустороннюю связь в режиме реального времени между веб-браузерами и сервером Node.js. Это высокопроизводительная и надежная библиотека, оптимизированная для обработки больших объемов данных с минимальной задержкой. Он следует протоколу WebSocket и обеспечивает улучшенные функциональные возможности, такие как возврат к длительному опросу HTTP или автоматическое повторное подключение, что позволяет нам создавать эффективные приложения в реальном времени.

Как создать соединение в реальном времени с помощью Socket.io и React.js

Здесь мы настроим среду проекта для проекта. Вы также узнаете, как добавить Socket.io в приложение React и Node.js и соединить оба сервера разработки для связи в реальном времени через Socket.io.

Создайте папку проекта, содержащую две подпапки с именами client и server.

mkdir todo-list
cd todo-list
mkdir client server

Перейдите в папку клиента через свой терминал и создайте новый проект React.js.

cd client
npx create-react-app ./

Установите клиентский API Socket.io и React Router. React Router — это библиотека JavaScript, которая позволяет нам перемещаться между страницами в приложении React.

npm install socket.io-client react-router-dom

Удалите лишние файлы, такие как логотип и тестовые файлы, из приложения React и обновите файл App.js, чтобы отобразить Hello World, как показано ниже.

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;

Перейдите в папку сервера и создайте файл package.json.

cd server & npm init -y

Установите Express.js, CORS, Nodemon и Socket.io Server API.

Express.js — это быстрый минималистичный фреймворк, предоставляющий несколько функций для создания веб-приложений на Node.js. CORS — это пакет Node.js, который обеспечивает связь между разными доменами.

Nodemon — это инструмент Node.js, который автоматически перезапускает сервер после обнаружения изменений в файле, а Socket.io позволяет нам настроить соединение в реальном времени на сервере.

npm install express cors nodemon socket.io

Создайте файл index.js — точку входа на веб-сервер.

touch index.js

Настройте простой сервер Node.js с помощью Express.js. Приведенный ниже фрагмент кода возвращает объект JSON при посещении http://localhost:4000/api в браузере.

//👇🏻index.js
const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get("/api", (req, res) => {
    res.json({
        message: "Hello world",
    });
});
app.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});

Импортируйте библиотеки HTTP и CORS, чтобы разрешить передачу данных между клиентским и серверным доменами.

const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
//New imports
const http = require("http").Server(app);
const cors = require("cors");
app.use(cors());
app.get("/api", (req, res) => {
    res.json({
        message: "Hello world",
    });
});
http.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});

Затем добавьте Socket.io в проект, чтобы создать соединение в реальном времени. Перед app.get()блоком скопируйте приведенный ниже код.

//New imports
.....
const socketIO = require('socket.io')(http, {
    cors: {
        origin: "http://localhost:3000"
    }
});
//Add this before the app.get() block
socketIO.on('connection', (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);
    socket.on('disconnect', () => {
            socket.disconnect()
      console.log('🔥: A user disconnected');
    });
});

Из приведенного выше фрагмента кода функция socket.io("connection") устанавливает соединение с приложением React, затем создает уникальный идентификатор для каждого сокета и записывает идентификатор в консоль всякий раз, когда пользователь посещает веб-страницу.

Когда вы обновляете или закрываете веб-страницу, сокет запускает событие разъединения, показывающее, что пользователь отключился от сокета.

Настройте Nodemon, добавив команду запуска в список скриптов в файле package.json. Фрагмент кода ниже запускает сервер с помощью Nodemon.

//In server/package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js"
  },

Теперь вы можете запустить сервер с помощью Nodemon, используя приведенную ниже команду.

npm start

Создание пользовательского интерфейса

Здесь мы создадим пользовательский интерфейс для приложения. Оно разделено на три страницы: страница входа, страница задач — центральная часть приложения и страница комментариев — где пользователи могут комментировать каждую задачу.

Перейдите в client/src и создайте папку компонентов, содержащую файлы Login.js, Task.js и Comments.js.

cd client/src
mkdir components
cd components
touch Login.js Task.js Comments.js

Обновите файл App.js, чтобы отображать вновь созданные компоненты на разных маршрутах через React Router.

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Comments from "./components/Comments";
import Task from "./components/Task";
import Login from "./components/Login";
function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path='/' element={<Login />} />
                <Route path='/task' element={<Task />} />
                <Route path='/comments/:category/:id' element={<Comments />} />
            </Routes>
        </BrowserRouter>
    );
}
export default App;

Перейдите в файл src/index.css и скопируйте приведенный ниже код. Он содержит весь CSS, необходимый для стилизации этого проекта.

@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
* {
    font-family: "Space Grotesk", sans-serif;
    box-sizing: border-box;
}
a {
    text-decoration: none;
}
body {
    margin: 0;
    padding: 0;
}
.navbar {
    width: 100%;
    background-color: #f1f7ee;
    height: 10vh;
    border-bottom: 1px solid #ddd;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px;
}
.form__input {
    min-height: 20vh;
    display: flex;
    align-items: center;
    justify-content: center;
}
.input {
    margin: 0 5px;
    width: 50%;
    padding: 10px 15px;
}
.addTodoBtn {
    width: 150px;
    padding: 10px;
    cursor: pointer;
    background-color: #367e18;
    color: #fff;
    border: none;
    outline: none;
    height: 43px;
}
.container {
    width: 100%;
    min-height: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px;
}
.completed__wrapper,
.ongoing__wrapper,
.pending__wrapper {
    width: 32%;
    min-height: 60vh;
    display: flex;
    flex-direction: column;
    padding: 5px;
}
.ongoing__wrapper > h3,
.pending__wrapper > h3,
.completed__wrapper > h3 {
    text-align: center;
    text-transform: capitalize;
}
.pending__items {
    background-color: #eee3cb;
}
.ongoing__items {
    background-color: #d2daff;
}
.completed__items {
    background-color: #7fb77e;
}
.pending__container,
.ongoing__container,
.completed__container {
    width: 100%;
    min-height: 55vh;
    display: flex;
    flex-direction: column;
    padding: 5px;
    border: 1px solid #ddd;
    border-radius: 5px;
}
.pending__items,
.ongoing__items,
.completed__items {
    width: 100%;
    border-radius: 5px;
    margin-bottom: 10px;
    padding: 15px;
}
.comment {
    text-align: right;
    font-size: 14px;
    cursor: pointer;
    color: rgb(85, 85, 199);
}
.comment:hover {
    text-decoration: underline;
}
.comments__container {
    padding: 20px;
}
.comment__form {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    margin-bottom: 30px;
}
.comment__form > label {
    margin-bottom: 15px;
}
.comment__form textarea {
    width: 80%;
    padding: 15px;
    margin-bottom: 15px;
}
.commentBtn {
    padding: 10px;
    width: 200px;
    background-color: #367e18;
    outline: none;
    border: none;
    color: #fff;
    height: 45px;
    cursor: pointer;
}
.comments__section {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
}
.login__form {
    width: 100%;
    height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.login__form > label {
    margin-bottom: 15px;
}
.login__form > input {
    width: 70%;
    padding: 10px 15px;
    margin-bottom: 15px;
}
.login__form > button {
    background-color: #367e18;
    color: #fff;
    padding: 15px;
    cursor: pointer;
    border: none;
    font-size: 16px;
    outline: none;
    width: 200px;
}

Страница входа

Здесь приложение принимает имя пользователя и сохраняет его в локальном хранилище для идентификации.

Обновите файл Login.js, как показано ниже:

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
const Login = () => {
    const [username, setUsername] = useState("");
    const navigate = useNavigate();
    const handleLogin = (e) => {
        e.preventDefault();
        //👇🏻 saves the username to localstorage
        localStorage.setItem("userId", username);
        setUsername("");
        //👇🏻 redirects to the Tasks page.
        navigate("/tasks");
    };
    return (
        <div className='login__container'>
            <form className='login__form' onSubmit={handleLogin}>
                <label htmlFor='username'>Provide a username</label>
                <input
                    type='text'
                    name='username'
                    id='username'
                    required
                    onChange={(e) => setUsername(e.target.value)}
                    value={username}
                />
                <button>SIGN IN</button>
            </form>
        </div>
    );
};
export default Login;

Страница задач

Здесь я проведу вас через создание веб-макета для страницы задач. На изображении ниже представлен макет страницы.

Разделите макет на три компонента, а именно: Nav.js, AddTask.js — раздел ввода формы и TasksContainer.js — содержащий задачи.

cd src/components
touch Nav.js AddTask.js TasksContainer.js

Визуализируйте компоненты в файле Task.js.

import React from "react";
import AddTask from "./AddTask";
import TasksContainer from "./TasksContainer";
import Nav from "./Nav";
import socketIO from "socket.io-client";
/*
👇🏻  Pass Socket.io into the required components
    where communications are made with the server
*/
const socket = socketIO.connect("http://localhost:4000");
const Task = () => {
    return (
        <div>
            <Nav />
            <AddTask socket={socket} />
            <TasksContainer socket={socket} />
        </div>
    );
};
export default Task;

Скопируйте приведенный ниже код в файл Nav.js.

import React from "react";
const Nav = () => {
    return (
        <nav className='navbar'>
            <h3>Team's todo list</h3>
        </nav>
    );
};
export default Nav;

Обновите файл AddTask.js, как показано ниже:

import React, { useState } from "react";
const AddTask = ({ socket }) => {
    const [task, setTask] = useState("");
    const handleAddTodo = (e) => {
        e.preventDefault();
        //👇🏻 Logs the task to the console
        console.log({ task });
        setTask("");
    };
    return (
        <form className='form__input' onSubmit={handleAddTodo}>
            <label htmlFor='task'>Add Todo</label>
            <input
                type='text'
                name='task'
                id='task'
                value={task}
                className='input'
                required
                onChange={(e) => setTask(e.target.value)}
            />
            <button className='addTodoBtn'>ADD TODO</button>
        </form>
    );
};
export default AddTask;

Скопируйте приведенный ниже код в файл TasksContainer.js. Он отображает три родительских элемента для ожидающих, текущих и завершенных задач.

import React from "react";
import { Link } from "react-router-dom";
const TasksContainer = ({ socket }) => {
    return (
        <div className='container'>
            <div className='pending__wrapper'>
                <h3>Pending Tasks</h3>
                <div className='pending__container'>
                    <div className='pending__items'>
                        <p>Debug the Notification center</p>
                        <p className='comment'>
                            <Link to='/comments'>2 Comments</Link>
                        </p>
                    </div>
                </div>
            </div>
            <div className='ongoing__wrapper'>
                <h3>Ongoing Tasks</h3>
                <div className='ongoing__container'>
                    <div className='ongoing__items'>
                        <p>Create designs for Novu</p>
                        <p className='comment'>
                            <Link to='/comments'>Add Comment</Link>
                        </p>
                    </div>
                </div>
            </div>
            <div className='completed__wrapper'>
                <h3>Completed Tasks</h3>
                <div className='completed__container'>
                    <div className='completed__items'>
                        <p>Debug the Notification center</p>
                        <p className='comment'>
                            <Link to='/comments'>2 Comments</Link>
                        </p>
                    </div>
                </div>
            </div>
        </div>
    );
};
export default TasksContainer;

Поздравляем!💃🏻 Макет настроен. Итак, давайте создадим простой шаблон для страницы комментариев.

Страница комментариев

Скопируйте приведенный ниже код в файл Comments.js. Он записывает комментарий и имя пользователя в консоль.

import React, { useEffect, useState } from "react";
import socketIO from "socket.io-client";
import { useParams } from "react-router-dom";
const socket = socketIO.connect("http://localhost:4000");
const Comments = () => {
    const [comment, setComment] = useState("");
    const addComment = (e) => {
        e.preventDefault();
        console.log({
            comment,
            userId: localStorage.getItem("userId"),
        });
        setComment("");
    };
    return (
        <div className='comments__container'>
            <form className='comment__form' onSubmit={addComment}>
                <label htmlFor='comment'>Add a comment</label>
                <textarea
                    placeholder='Type your comment...'
                    value={comment}
                    onChange={(e) => setComment(e.target.value)}
                    rows={5}
                    id='comment'
                    name='comment'
                    required
                ></textarea>
                <button className='commentBtn'>ADD COMMENT</button>
            </form>
            <div className='comments__section'>
                <h2>Existing Comments</h2>
                <div></div>
            </div>
        </div>
    );
};
export default Comments;

Теперь пользовательский интерфейс завершен. Затем давайте добавим React Beautiful DND в приложение, чтобы включить функцию перетаскивания.

Как добавить функцию перетаскивания с помощью React Beautiful DND

Здесь вы узнаете, как добавить функцию перетаскивания с помощью React Beautiful DND и установить связь между приложением React и сервером Socket.io Node.js.

Как работает React Beautiful DND?

React Beautiful DND — это высокопроизводительная библиотека, которая позволяет нам выбирать и перетаскивать элемент из его текущей позиции в другую позицию на странице.

На изображении выше показано, как настроить React Beautiful DND. Вы должны обернуть все перетаскиваемые и сбрасываемые элементы в файл <DragDropContext/>. Компонент <Droppable/> содержит перетаскиваемые элементы, размещенные внутри компонента <Draggable/>.

Делаем задачи перетаскиваемыми и сбрасываемыми с помощью React Beautiful DND

Здесь вы узнаете, как добавить React Beautiful DND в приложение React и сделать задачи перемещаемыми из одной категории в другую (ожидающие, текущие и завершенные).

Установите React Beautiful DND и убедитесь, что вы не используете React в строгом режиме. (проверьте src/index.js).

npm install react-beautiful-dnd

Откройте файл server/index.js и создайте объект, содержащий все фиктивные данные для каждой категории задач.

//👇🏻 server/index.js
//👇🏻 Generates a random string
const fetchID = () => Math.random().toString(36).substring(2, 10);
//👇🏻 Nested object
let tasks = {
    pending: {
        title: "pending",
        items: [
            {
                id: fetchID(),
                title: "Send the Figma file to Dima",
                comments: [],
            },
        ],
    },
    ongoing: {
        title: "ongoing",
        items: [
            {
                id: fetchID(),
                title: "Review GitHub issues",
                comments: [
                    {
                        name: "David",
                        text: "Ensure you review before merging",
                        id: fetchID(),
                    },
                ],
            },
        ],
    },
    completed: {
        title: "completed",
        items: [
            {
                id: fetchID(),
                title: "Create technical contents",
                comments: [
                    {
                        name: "Dima",
                        text: "Make sure you check the requirements",
                        id: fetchID(),
                    },
                ],
            },
        ],
    },
};
//👇🏻 host the tasks object via the /api route
app.get("/api", (req, res) => {
    res.json(tasks);
});

Затем выберите задачи из файла TasksContainer.js. Фрагмент кода ниже преобразует объект tasks в массив перед визуализацией компонента.

import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const TasksContainer = () => {
    const [tasks, setTasks] = useState({});
    useEffect(() => {
        function fetchTasks() {
            fetch("http://localhost:4000/api")
                .then((res) => res.json())
                .then((data) => {
                    console.log(data);
                    setTasks(data);
                });
        }
        fetchTasks();
    }, []);
    return (
        <div className='container'>
            {/* 
            👇🏻 Returns an array of each tasks (Uncomment to view the data structure)
             {Object.entries(tasks).map((task) => console.log(task))} */}
            {Object.entries(tasks).map((task) => (
                <div
                    className={`${task[1].title.toLowerCase()}__wrapper`}
                    key={task[1].title}
                >
                    <h3>{task[1].title} Tasks</h3>
                    <div className={`${task[1].title.toLowerCase()}__container`}>
                        {task[1].items.map((item, index) => (
                            <div
                                className={`${task[1].title.toLowerCase()}__items`}
                                key={item.id}
                            >
                                <p>{item.title}</p>
                                <p className='comment'>
                                    <Link to='/comments'>
                                        {item.comments.length > 0 ? `View Comments` : "Add Comment"}
                                    </Link>
                                </p>
                            </div>
                        ))}
                    </div>
                </div>
            ))}
        </div>
    );
};
export default TasksContainer;

Импортируйте необходимые компоненты из «react-beautiful-dnd» в файл TasksContainer.js.

//👇🏻 At the top of the TasksContainer.js file
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

Обновите файл TaskContainer.js, как показано ниже:

return (
    <div className='container'>
        {/** --- 👇🏻 DragDropContext  ---- */}
        <DragDropContext onDragEnd={handleDragEnd}>
            {Object.entries(tasks).map((task) => (
                <div
                    className={`${task[1].title.toLowerCase()}__wrapper`}
                    key={task[1].title}
                >
                    <h3>{task[1].title} Tasks</h3>
                    <div className={`${task[1].title.toLowerCase()}__container`}>
                        {/** --- 👇🏻 Droppable --- */}
                        <Droppable droppableId={task[1].title}>
                            {(provided) => (
                                <div ref={provided.innerRef} {...provided.droppableProps}>
                                    {task[1].items.map((item, index) => (
                                            {/** --- 👇🏻 Draggable --- */}
                                        <Draggable
                                            key={item.id}
                                            draggableId={item.id}
                                            index={index}
                                        >
                                            {(provided) => (
                                                <div
                                                    ref={provided.innerRef}
                                                    {...provided.draggableProps}
                                                    {...provided.dragHandleProps}
                                                    className={`${task[1].title.toLowerCase()}__items`}
                                                >
                                                    <p>{item.title}</p>
                                                    <p className='comment'>
                                                        <Link to={`/comments/${task[1].title}/${item.id}`}>
                                                            {item.comments.length > 0
                                                                ? `View Comments`
                                                                : "Add Comment"}
                                                        </Link>
                                                    </p>
                                                </div>
                                            )}
                                        </Draggable>
                                    ))}
                                    {provided.placeholder}
                                </div>
                            )}
                        </Droppable>
                    </div>
                </div>
            ))}
        </DragDropContext>
    </div>
);
  • Из фрагмента кода выше:
  • DragDropContext обертывает весь контейнер перетаскивания, а Droppable представляет родительский элемент для перетаскиваемых элементов.
  • Компоненты Draggable и Droppable принимают перетаскиваемый идентификатор. Они также принимают дочерний элемент, provided, который позволяет нам ссылаться и отображать каждый элемент как перетаскиваемый элемент.
  • Не стесняйтесь разделить код на разные компоненты и нажмите здесь, чтобы узнать больше о React Beautiful DND.

DragDropContext принимает реквизит onDragEnd, который срабатывает сразу после перетаскивания элемента.

//👇🏻 This function is the value of the onDragEnd prop
const handleDragEnd = ({ destination, source }) => {
    if (!destination) return;
    if (
        destination.index === source.index &&
        destination.droppableId === source.droppableId
    )
        return;
    socket.emit("taskDragged", {
        source,
        destination,
    });
};

Приведенный выше фрагмент кода принимает место назначения и источник перетаскиваемого элемента, проверяет, был ли он перетащен в место назначения, которое можно перетаскивать, и не совпадают ли источник и место назначения перед отправкой сообщения на сервер Node.js через Socket.io.

Создайте прослушиватель события taskDragged на серверной части.

socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);
    socket.on("taskDragged", (data) => {
        console.log(data);
    });
    socket.on("disconnect", () => {
        socket.disconnect();
        console.log("🔥: A user disconnected");
    });
});

Давайте кратко рассмотрим данные, возвращаемые после перетаскивания элемента:

Фрагмент кода ниже показывает, что элемент перемещен из категории «Ожидание» в категорию «Текущие». Индекс также изменился с 0 на 1.

{
  source: { index: 0, droppableId: 'pending' },
  destination: { droppableId: 'ongoing', index: 1 }
}

Затем сделайте так, чтобы перетаскиваемый элемент оставался в месте назначения. Обновите прослушиватель taskDragged, как показано ниже:

socket.on("taskDragged", (data) => {
    const { source, destination } = data;
    //👇🏻 Gets the item that was dragged
    const itemMoved = {
        ...tasks[source.droppableId].items[source.index],
    };
    console.log("DraggedItem>>> ", itemMoved);
    //👇🏻 Removes the item from the its source
    tasks[source.droppableId].items.splice(source.index, 1);
    //👇🏻 Add the item to its destination using its destination index
    tasks[destination.droppableId].items.splice(destination.index, 0, itemMoved);
    //👇🏻 Sends the updated tasks object to the React app
    socket.emit("tasks", tasks);
    /* 👇🏻 Print the items at the Source and Destination
        console.log("Source >>>", tasks[source.droppableId].items);
        console.log("Destination >>>", tasks[destination.droppableId].items);
        */
});

Создайте прослушиватель для события tasks в компоненте TasksContainer.

useEffect(() => {
    socket.on("tasks", (data) => setTasks(data));
}, [socket]);

Поздравляем!🎉 Теперь вы можете перетаскивать элементы из одной категории в другую.

Как создавать новые задачи

В этом разделе я проведу вас через создание новых задач из приложения React.

Обновите файл AddTask.js, чтобы отправить новую задачу на внутренний сервер.

import React, { useState } from "react";
const AddTask = ({ socket }) => {
    const [task, setTask] = useState("");
    const handleAddTodo = (e) => {
        e.preventDefault();
        //👇🏻 sends the task to the Socket.io server
        socket.emit("createTask", { task });
        setTask("");
    };
    return (
        <form className='form__input' onSubmit={handleAddTodo}>
            <label htmlFor='task'>Add Todo</label>
            <input
                type='text'
                name='task'
                id='task'
                value={task}
                className='input'
                required
                onChange={(e) => setTask(e.target.value)}
            />
            <button className='addTodoBtn'>ADD TODO</button>
        </form>
    );
};
export default AddTask;

Создайте прослушиватель события createTask на внутреннем сервере и добавьте элемент в объект tasks.

socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);
    socket.on("createTask", (data) => {
        // 👇🏻 Constructs an object according to the data structure
        const newTask = { id: fetchID(), title: data.task, comments: [] };
        // 👇🏻 Adds the task to the pending category
        tasks["pending"].items.push(newTask);
        /* 
        👇🏻 Fires the tasks event for update
         */
        socket.emit("tasks", tasks);
    });
    //...other listeners
});

Заполнение страницы комментариев

В этом разделе вы узнаете, как добавлять и получать комментарии к каждой задаче.

Обновите файл Comments.js, как показано ниже:

import React, { useEffect, useState } from "react";
import socketIO from "socket.io-client";
import { useParams } from "react-router-dom";
const socket = socketIO.connect("http://localhost:4000");
const Comments = () => {
    const { category, id } = useParams();
    const [comment, setComment] = useState("");
    const addComment = (e) => {
        e.preventDefault();
        /*
        👇🏻 sends the comment, the task category, item's id and the userID.
         */
        socket.emit("addComment", {
            comment,
            category,
            id,
            userId: localStorage.getItem("userId"),
        });
        setComment("");
    };
    return (
        <div className='comments__container'>
            <form className='comment__form' onSubmit={addComment}>
                <label htmlFor='comment'>Add a comment</label>
                <textarea
                    placeholder='Type your comment...'
                    value={comment}
                    onChange={(e) => setComment(e.target.value)}
                    rows={5}
                    id='comment'
                    name='comment'
                    required
                ></textarea>
                <button className='commentBtn'>ADD COMMENT</button>
            </form>
            <div className='comments__section'>
                <h2>Existing Comments</h2>
                <div></div>
            </div>
        </div>
    );
};
export default Comments;

Напомним, что маршрут страницы комментариев — /comments/:category/:id; приведенный выше фрагмент кода извлекает категорию элемента и его идентификатор из URL-адреса страницы, а затем отправляет категорию элемента, идентификатор, идентификатор пользователя и комментарий на сервер Node.js.

Затем создайте прослушиватель событий на сервере Node.js, который добавит комментарий к конкретной задаче через ее идентификатор.

socket.on("addComment", (data) => {
    const { category, userId, comment, id } = data;
    //👇🏻 Gets the items in the task's category
    const taskItems = tasks[category].items;
    //👇🏻 Loops through the list of items to find a matching ID
    for (let i = 0; i < taskItems.length; i++) {
        if (taskItems[i].id === id) {
    //👇🏻 Then adds the comment to the list of comments under the item (task)
            taskItems[i].comments.push({
                name: userId,
                text: comment,
                id: fetchID(),
            });
            //👇🏻 sends a new event to the React app
            socket.emit("comments", taskItems[i].comments);
        }
    }
});

Получить комментарии через Socket.io.

const Comments = () => {
    const { category, id } = useParams();
    const [comment, setComment] = useState("");
    const [commentList, setCommentList] = useState([]);
    //👇🏻 Listens to the comments event
    useEffect(() => {
        socket.on("comments", (data) => setCommentList(data));
    }, []);
    //...other listeners
    return (
        <div className='comments__container'>
            <form className='comment__form' onSubmit={addComment}>
                ...
            </form>
            {/** 👇🏻 Displays all the available comments*/}
            <div className='comments__section'>
                <h2>Existing Comments</h2>
                {commentList.map((comment) => (
                    <div key={comment.id}>
                        <p>
                            <span style={{ fontWeight: "bold" }}>{comment.text} </span>by{" "}
                            {comment.name}
                        </p>
                    </div>
                ))}
            </div>
        </div>
    );
};
export default Comments;

Наконец, добавьте этот хук useEffect, чтобы получать комментарии, когда страница загружается в браузер.

useEffect(() => {
    socket.emit("fetchComments", { category, id });
}, [category, id]);

Прослушайте событие на бэкенде и инициируйте событие комментариев, чтобы вернуть список комментариев, соответствующих идентификатору и категории элемента.

socket.on("fetchComments", (data) => {
    const { category, id } = data;
    const taskItems = tasks[category].items;
    for (let i = 0; i < taskItems.length; i++) {
        if (taskItems[i].id === id) {
            socket.emit("comments", taskItems[i].comments);
        }
    }
});

Поздравляем!💃🏻 Мы завершили этот проект.

ДОПОЛНИТЕЛЬНО: Отправка уведомлений с Novu

Если вы хотите добавить уведомления в приложение, когда пользователь добавляет комментарий или новую задачу, вы можете легко сделать это с помощью Novu в компоненте Nav.js.

Novu позволяет добавлять различные типы уведомлений, такие как электронная почта, SMS и уведомления в приложении.

Как добавить Novu в приложение React и Node.js

Чтобы добавить уведомление в приложении, установите SDK Novu Node.js на сервер и Центр уведомлений в приложении React.

👇🏻 Install on the client
npm install @novu/notification-center
👇🏻 Install on the server
npm install @novu/node

Создайте проект Novu, запустив приведенный ниже код. Вам доступна персонализированная панель управления.

👇🏻 Install on the client
npx novu init

Вам нужно будет войти в Github перед созданием проекта Novu. Фрагмент кода ниже содержит шаги, которые вы должны выполнить после запуска npx novu init.

Now let's setup your account and send your first notification
❓ What is your application name? Devto Clone
❓ Now lets setup your environment. How would you like to proceed?
   > Create a free cloud account (Recommended)
❓ Create your account with:
   > Sign-in with GitHub
❓ I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy)
    > Yes
✔️ Create your account successfully.
We've created a demo web page for you to see novu notifications in action.
Visit: http://localhost:57807/demo to continue

Посетите демонстрационную веб-страницу http://localhost:57807/demo, скопируйте свой идентификатор подписчика со страницы и нажмите кнопку «Пропустить руководство». Мы будем использовать его позже в этом уроке.

Обновите файл components/Nav.js, чтобы он содержал Novu и его необходимые элементы для уведомлений в приложении из документации.

import React from "react";
import {
    NovuProvider,
    PopoverNotificationCenter,
    NotificationBell,
} from "@novu/notification-center";
import { useNavigate } from "react-router-dom";
const Nav = () => {
    const navigate = useNavigate();
    const onNotificationClick = (notification) =>
        navigate(notification.cta.data.url);
    return (
        <nav className='navbar'>
            <h3>Team's todo list</h3>
            <div>
                <NovuProvider
                    subscriberId='<SUBSCRIBER_ID>'
                    applicationIdentifier='<APP_ID>'
                >
                    <PopoverNotificationCenter
                        onNotificationClick={onNotificationClick}
                        colorScheme='light'
                    >
                        {({ unseenCount }) => (
                            <NotificationBell unseenCount={unseenCount} />
                        )}
                    </PopoverNotificationCenter>
                </NovuProvider>
            </div>
        </nav>
    );
};
export default Nav;

Приведенный выше фрагмент кода добавляет значок колокольчика уведомлений Novu в компонент Nav, что позволяет нам просматривать все уведомления из приложения.

💡 Для компонента NovuProvider требуется ваш идентификатор подписчика, скопированный ранее из http://localhost:57807/demo, и идентификатор вашего приложения, доступный в разделе Настройки в разделе Ключи API на Novu Manage Platform.

Далее давайте создадим рабочий процесс для приложения, который описывает функции, которые вы хотите добавить в приложение.

Выберите «Уведомление» на боковой панели «Разработка» и создайте шаблон уведомления. Выберите только что созданный шаблон, нажмите «Редактор рабочего процесса» и убедитесь, что рабочий процесс выглядит следующим образом:

На изображении выше Novu запускает механизм дайджеста перед отправкой уведомления в приложении.

Novu Digest позволяет нам контролировать, как мы хотим отправлять уведомления в приложении. Он собирает несколько триггерных событий и отправляет их как одно сообщение. Изображение выше отправляет уведомления каждые 2 минуты, и это может быть эффективно, когда у вас много пользователей и частые обновления.

Нажмите на шаг In-App и отредактируйте шаблон уведомления, чтобы он содержал приведенное ниже содержимое.

{{userId}} added a new task.

💡 Novu позволяет добавлять в шаблоны динамический контент или данные с помощью движка шаблонов Handlebars. Данные для переменной имени пользователя будут вставлены в шаблон в качестве полезной нагрузки из запроса.

Сохраните шаблон, нажав кнопку Update, и вернитесь в редактор кода.

Добавление Нову в приложение

Импортируйте Novu из пакета и создайте экземпляр, используя свой ключ API на сервере.

//server/index.js
const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");

Создайте функцию, которая отправляет уведомление через Novu в приложение React.

const sendNotification = async (user) => {
    try {
        const result = await novu.trigger(<TEMPLATE_ID>, {
            to: {
                subscriberId: <SUBSCRIBER_ID>,
            },
            payload: {
                userId: user,
            },
        });
        console.log(result);
    } catch (err) {
        console.error("Error >>>>", { err });
    }
};
//👇🏻 The function is called after a new task is created
socket.on("createTask", (data) => {
        const newTask = { id: fetchID(), title: data.task, comments: [] };
        tasks["pending"].items.push(newTask);
        socket.emit("tasks", tasks);
//👇🏻 Triggers the notification via Novu
        sendNotification(data.userId);
    });

Приведенный выше фрагмент кода отправляет уведомление всем пользователям, когда в приложение добавляется новая задача.

Заключение

Итак, вы узнали, как настроить Socket.io в приложении React и Node.js, общаться между сервером и клиентом через Socket.io и перетаскивать элементы с помощью React Beautiful DND.

Это демонстрация того, что вы можете создать, используя Socket.io и React Beautiful DND. Не стесняйтесь улучшать приложение, добавляя аутентификацию, возможность назначать задачи конкретному пользователю и добавлять уведомления, когда пользователь оставляет комментарий.

Исходный код этого руководства доступен здесь: https://github.com/novuhq/blog/tree/main/react-beautiful-dnd-todo-list

Спасибо за чтение!

P.S. Novu присылает потрясающие подарки на Hacktoberfest! Рады, если вы можете поддержать нас, поставив нам звезду! ⭐️



Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord.