title
Trong hướng dẫn này, chúng ta sẽ tạo một ứng dụng việc cần làm đơn giản bằng cách sử dụng React và ghi chú dán có thể kéo được.
Ứng dụng sẽ cho phép người dùng click vào mọi vị trí trên màn hình để thêm, chỉnh sửa và xóa các tác vụ được biểu thị dưới dạng ghi chú dán và có thể kéo được trên màn hình. Màu sắc, độ nghiêng sẽ được ramdom trong mảng cho trước.
Doubleclick vào ghi chú sẽ sửa được nội dung của ghi chú.

Demo: https://notetaker-gray-nine.vercel.app
Source code: https://github.com/ducanhle31/notetaker

Hãy đi sâu vào quy trình từng bước để xây dựng ứng dụng này.

Bước 1: Thiết lập dự án

Bắt đầu bằng cách tạo một dự án React mới bằng cách sử dụng Tạo ứng dụng React + Vite

  • Mở terminal của bạn và chạy lệnh sau để khởi tạo dự án React mới:
npm create vite@latest note
  • Sau đó làm theo hướng dẫn và tạo ứng dụng React bằng Vite.
  • Khi dự án được tạo, hãy điều hướng vào thư mục dự án:
cd note
  • Cài đặt các thư viện:
npm i react-draggable

Bước 2: Tạo một tệp mới có tên Note.jsx trong thư mục src và thêm các đoạn mã dưới đây.

Import các thành phần

import React, { useState, useEffect, useRef } from "react";
import Draggable from "react-draggable";
Note.jsx

Trong hàm Note, chúng ta sử dụng các hook useState và useEffect, useRef để quản lý trạng thái, hiệu suất , đối tượng tham chiếu .

  • Khởi tạo các state để tạo mới và lấy vị trí, màu sắc và độ nghiêng của ghi chú
export default function Note() {
      //Khởi tạo state lưu giá trị người dùng nhập vào ô input
     const [task, setTask] = useState("");
      // khởi tạo state lưu trữ mảng Note
     const [tasks, setTasks] = useState([]);
     // Lưu trữ vị trí nhấp chuột của người dùng trên trang.
     const [clickPosition, setClickPosition] = useState({ x: null, y: null });
     // Chuyển đổi để hiển thị/ẩn trường nhập liệu để thêm tác vụ mới.
     const [showResults, setShowResults] = useState(false);
      // lưu màu nền , giá trị transform, Font chữ
      const [background, setBackground] = useState("null");
      const [transform, setTransform] = useState("null");
      const [randomFonts, setRandomFonts] = useState("Kalam, cursive");
  //  useEffect tải mọi tác vụ được lưu trữ trước đó từ localStorage khi thành phần được gắn kết.
 useEffect(() => {
    if (localStorage.getItem("localTasks")) {
      const storedList = JSON.parse(localStorage.getItem("localTasks"));
      setTasks(storedList);
    }
  }, []);
 return (
        ....
    )}
Note.jsx
  • Tiếp theo khởi tạo state sửa giá trị nội dung, vị trí của ghi chú
export default function Note() {
      //khởi tạo state sửa giá trị theo Id
     const [todoEditing, setTodoEditing] = useState(null);
      //khởi tạo state sửa giá trị người dùng nhập vào ô input
     const [editingText, setEditingText] = useState("");
      // khởi tạo state Lưu trữ vị trí nhấp chuột của việc cần làm đang được chỉnh sửa.
     const [editingClickPosition, setEditingClickPosition] = useState("");
     // Lưu trữ vị trí của ghi chú dán có thể kéo được.
     const [position, setPosition] = useState(clickPosition);
  return (
        ....
    )}
Note.jsx
  • Cuối cùng khởi tạo tượng tham chiếu để kéo các thành phần
export default function Note() {
     // khởi tạo đối tượng tham chiếu để kéo các thành phần
      const draggableRef = useRef(null);
 return (
        ....
    )}
Note.jsx

Bước 3: Cập nhật State (xử lý sự kiện)

Mỗi khi state được cập nhật thì Component sẽ re-render (function được chạy lại và giao diện được cập nhật lại theo state).

  • Cập nhật nội dung và vị trí khi click vào màn hình để tạo mới ghi chú
export default function Note() {
   // Khởi tạo
   ...
   // Cập nhật
   
     /// handleClick: Được gọi khi người dùng nhấp vào trang. 
     ///Cập nhật vị trí nhấp chuột, màu nền và độ nghiêng của ghi chú dán.
  const handleClick = (event) => {
    const { clientX, clientY } = event;
    setClickPosition({ x: clientX, y: clientY });
    handleButtonBackground();
    handleTransform();
  };
 return (
        ....
    )}
Note.jsx
export default function Note() {
   // Khởi tạo
   ...
   // Cập nhật
     /// Tạo màu nền ngẫu nhiên cho ghi chú dán.
const colors = [ "#f8a5a5", "#ffdde1", "#f978ff", "#ffdb37", "#00f5ee", "#3cf066" ];
const handleButtonBackground = () => {
    const randomIndexBackground = Math.floor(Math.random() * colors.length);
    const randomElements = colors[randomIndexBackground];
    setBackground(randomElements);
  };
  //Tạo một giá trị biến đổi xoay ngẫu nhiên cho ghi chú dán.
  const handleTransform = () => {
    const randomIndexTransform = Math.floor(Math.random() * (8 - -8)) + -8;
    setTransform(randomIndexTransform);
  };

  const textStyles = {
    position: " absolute",
    top: clickPosition.y,
    left: clickPosition.x,
    backgroundColor: `${background}`,
    transform: `rotate(${transform}deg) `,
  };

  const fonts = [
    "Kalam, cursive",
    "'Sacramento', cursive",
    "Just Another Hand', cursive",
    "'Mali', cursive",
  ];
  // handleButtonClick Được gọi khi người dùng nhấp đúp vào ghi chú . 
  //Tạo phông chữ ngẫu nhiên cho ghi chú dán.
  const handleButtonClick = () => {
    const randomIndex = Math.floor(Math.random() * fonts.length);
    const randomElement = fonts[randomIndex];
    setRandomFonts(randomElement);
  };

 return (
        ....
    )}
Note.jsx
  • Cập nhật đối tượng mới và thêm nó vào mảng , đồng thời cập nhật localStorage.
export default function Note() {
     // addTask Được gọi khi người dùng thêm một tác vụ mới. Tạo một đối tượng  mới và thêm nó vào mảng ,
  // đồng thời cập nhật localStorage.
 const addTask = (e) => {
    if (task) {
      const newTask = {
        id: new Date().getTime().toString(),
        title: task,
        clickPosition: clickPosition,
        background: background,
        transform: transform,
        randomFonts: randomFonts,
        position: position,
      };
      setTasks([...tasks, newTask]);
      localStorage.setItem("localTasks", JSON.stringify([...tasks, newTask]));

      setShowResults(false);
    }
  };
    return (
        ....
    )}
Note.jsx
  • Cập nhật lại nội dung và vị trí khi sửa ,kéo thay đổi vị trí của ghi chú
export default function Note() {
   /// khai báo
              ...
  /// cập nhật
   
  // handleDrag: Được gọi khi người dùng kéo ghi chú dán. 
   //Cập nhật vị trí và xử lý các cập nhật trạng thái liên quan đến chỉnh sửa.
  const handleDrag = (e, ui) => {
    const { x, y } = ui;
    setPosition({ x, y });

    if (task && task.id) {
      setTodoEditing(task.id);
      setEditingClickPosition(task.position);
    }

    handleButtonBackground();
    handleTransform();
  };
  // submitEdits: Được gọi khi người dùng gửi các chỉnh sửa cho một việc cần làm. 
  //Cập nhật tiêu đề và vị trí của nhiệm vụ trong mảng nhiệm vụ.
  function submitEdits(id) {
    const updatedTasks = [...tasks].map((task) => {
      if (task.id === id) {
        task.title = editingText;
        task.position = editingClickPosition;
      }
      return task;
    });
    setTasks(updatedTasks);
    setTodoEditing(null);
  }


return ( ... )
}
Note.jsx
  • Xóa ghi chú
 //  handleDelete Được gọi khi người dùng xóa một tác vụ. Xóa tác vụ khỏi mảng tác vụ và cập nhật localStorage.
const handleDelete = (task) => {
    const deleted = tasks.filter((t) => t.id !== task.id);
    setTasks(deleted);
    localStorage.setItem("localTasks", JSON.stringify(deleted));
  };

};
Note.jsx

Bước 4: render dữ liệu

  • Đây là phần giao diện để hiển thị và tạo sticky notes (ghi chú dính) trên màn hình.
export default function Note() {
/// khởi tạo
...
/// cập nhật
...
return(
<div onClick={handleClick} >
      <div
        onClick={() => {
          if (task.trim() == "") {
            setShowResults(true);
          } else {
            setTask("");  }   }}
     style={{ height: "2200px", width: "100%", position: "relative" }} >
           
  {showResults ? (
          <div onBlur={addTask} 
            className="container "
            style={textStyles}
            onClick={(event) => event.stopPropagation()}    >
            <svg width="0" height="0">
              <defs>
                <clipPath id="stickyClip" clipPathUnits="objectBoundingBox">
                  <path
   d="M 0 0 Q 0 0.69, 0.03 0.96 0.03 0.96, 1 0.96 Q 0.96 0.69, 0.96 0 0.96 0, 0 0"
                    strokeLinejoin="round"
                    strokeLinecap="square"
                  />
                </clipPath>
              </defs>
            </svg>
            <textarea
              name="task"
              type="text"
              value={task}
              placeholder="Write your note..."
              className="form-control"
              onChange={(e) => setTask(e.target.value)}
              onDoubleClick={handleButtonClick}
              style={{
                fontFamily: `${randomFonts}`,
                backgroundColor: `${background}`,
              }}
            />
          </div>
        ) : null}

    // ...
      </div>
    </div>
)}
Note.jsx
  • Tạo ra một danh sách các sticky note và hiển thị chúng trên màn hình.
export default function Note() {
/// khởi tạo
...
/// cập nhật
...
return(
<div onClick={handleClick} >
      <div
        onClick={() => {
          if (task.trim() == "") {
            setShowResults(true);
          } else {
            setTask("");    } }}
   style={{ height: "2200px", width: "100%", position: "relative" }} >
        // ...   

  {tasks.map((task) => (
          <div
            key={task.id}
            style={{ transform: `rotate(${task.transform}deg) ` }}  >
            <Draggable
              ref={draggableRef}
              onDrag={handleDrag}
              onDragover={() => submitEdits(task.id)}     >
              <div
                style={{
                  top: `${task.clickPosition.y}px`,
                  left: `${task.clickPosition.x}px`,
                  position: "absolute",
                  transform: `rotate(${task.transform}deg) `,
                }}
                className="sticky-container"
                key={task.id}
                onClick={(event) => event.stopPropagation()}
              >
                <div className="sticky-outer">
                  <div className="sticky">
                    <svg width="0" height="0">
                      <defs>
                        <clipPath
                          id="stickyClip"
                          clipPathUnits="objectBoundingBox"
                        >
                          <path
    d="M 0 0 Q 0 0.69, 0.03 0.96 0.03 0.96, 1 0.96 Q 0.96 0.69, 0.96 0 0.96 0, 0 0"
                            strokeLinejoin="round"
                            strokeLinecap="square"
                          />
                        </clipPath>
                      </defs>
                    </svg>
                    <div
                      className="sticky-content"
                      style={{
                        backgroundColor: `${task.background}`,
                        fontFamily: `${task.randomFonts}`,
                      }}
                      onDoubleClick={() => {
                        setTodoEditing(task.id), setEditingText(task.title);
                      }}
                      onBlur={() => submitEdits(task.id)}
                    >
                      <p className="delete" onClick={() => handleDelete(task)}>
                        <i className="bi bi-trash3"></i>
                      </p>
                      {task.id === todoEditing ? (
                        <textarea placeholder="Write your note..."
                          className="form-control"
                          value={editingText}
                          type="text"
                          onChange={(e) => setEditingText(e.target.value)}
                          style={{
                            backgroundColor: `${task.background}`,
                            fontFamily: `${task.randomFonts}`,
                          }}
                        />
                      ) : (
                        <p className="til"
                          style={{ fontFamily: `${task.randomFonts}` }}    >
                       {task.title}
                        </p>
                      )}
                    </div>
                  </div>
                </div>
              </div>
            </Draggable>
          </div>
        ))}
      </div>
    </div>
)}
Note.jsx

Bước 5: Tích hợp Thành phần Note

Mở tệp App.js trong thư mục src và thay thế mã hiện có bằng mã sau:

import "./App.css";
import Note from "./Note";

function App() {
  return (
    <>
      <h4>Chạm để tạo Notes mới</h4>
      <Note />
    </>
  );
}

export default App;
App.js

Bước 6: Chạy ứng dụng :

npm run dev

Kết luận

Bài viết này sử dụng các Hook của react và ứng dụng chúng một cách basic nhất.
Hãy làm theo cùng với hướng dẫn thực hành để bạn có thể tạo một ứng dụng ghi chú.
Cảm ơn vì đã đọc.