DEV Community 👩‍💻👨‍💻

Cover image for Building a Kanban style todo app
vigneshiyergithub
vigneshiyergithub

Posted on

Building a Kanban style todo app

What is this post about ?

In this article we will explore how to build a Kanban style todo app. Each todo task will have 3 possible states : Not started, In Progress, Done. As soon as a new todo is added the default state would be Not started and the user will be able to drag between states. This app is not focused on styling rather on the bare minimum functional implementation.

Check out the app here : Kanban style todo App

Kanban style todo app

Content

  • Design
  • Add new todo
  • Changing todo state
  • Deleting todo

Lets go deep dive into each one and explore how it was implemented.

Design

Design

Add new todo

Todo will be added using Input area and the default state will be Not Started.

// Component
const TodoInput = ({ onTodoAdd }) => {
  const [todoInput, setTodoInput] = useState("");
  const onAdd = (e) => {
    e.preventDefault();
    onTodoAdd(todoInput);
    setTodoInput("");
  };
  return (
    <form className="todo-input">
      <input
        placeholder="Add Todo entry"
        value={todoInput}
        onChange={(e) => setTodoInput(e.target.value)}
      />
      <button onClick={onAdd} type="submit">
        Add
      </button>
    </form>
  );
};

// onTodoAdd Prop implementation
const onTodoAdd = (todoText) => {
    setTodos((_t) => {
      return [
        ..._t,
        {
          id: uuidv4(), // Generate unique id 
          value: todoText,
          state: TODO_STATE.NOT_STARTED,
        },
      ];
    });
  };
Enter fullscreen mode Exit fullscreen mode

Changing todo state

Depending on the states for the todo we will need that many state containers to hold the todo. So in this case there will be 3 containers [Not started, In Progress, Done].
Each container will act as drop zone for the items to be dropped on.
Each task item will act as draggable item and can be dropped in any of the drop zones available.
Drop zones : Not started , In Progress, Done, Delete.
For implementing drag and drop functionality we will be using React DnD

export const TODO_STATE = {
  NOT_STARTED: "Not started",
  IN_PROGRESS: "In progress",
  DONE: "Done",
};

const TodoContent = ({ todos, onTodoDrag, onTodoDelete }) => {
  const notStartedTodos = getTodosBasedOnState(todos, TODO_STATE.NOT_STARTED);
  const inProgressTodos = getTodosBasedOnState(todos, TODO_STATE.IN_PROGRESS);
  const doneTodos = getTodosBasedOnState(todos, TODO_STATE.DONE);
  const [isDragActive, setIsDragActive] = useState(false);
  const onDragActive = (dragActive) => {
    setIsDragActive(dragActive);
  };
  return (
    <DndProvider backend={HTML5Backend}>
      <div className="todo-content">
        <DraggableItemContainer
          title="Not Started"
          todos={notStartedTodos}
          onTodoDrag={onTodoDrag}
          state={TODO_STATE.NOT_STARTED}
          onDragActive={onDragActive}
          onTodoDelete={onTodoDelete}
        />
        <DraggableItemContainer
          title="In Progress"
          todos={inProgressTodos}
          onTodoDrag={onTodoDrag}
          state={TODO_STATE.IN_PROGRESS}
          onDragActive={onDragActive}
          onTodoDelete={onTodoDelete}
        />
        <DraggableItemContainer
          title="Done"
          todos={doneTodos}
          onTodoDrag={onTodoDrag}
          state={TODO_STATE.DONE}
          onDragActive={onDragActive}
          onTodoDelete={onTodoDelete}
        />
      </div>
      {isDragActive && (
        <div className="delete-drag-container">
          <DeleteDragItemBox />
        </div>
      )}
    </DndProvider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Draggable Container

const DraggableItemContainer = ({
  title = "",
  todos = [],
  onTodoDrag,
  state,
  onDragActive,
  onTodoDelete,
}) => {
  const [{ canDrop, isOver }, drop] = useDrop(() => ({
    accept: ITEM_TYPES.TODO,
    drop: () => ({ state }),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  }));
  const isActive = canDrop && isOver;
  const style = {
    border: isActive ? "3px dashed black" : "1px solid black",
  };
  return (
    <div className="draggable-item-container" ref={drop} style={style}>
      <h4 className="title">
        {title} - [{todos.length}]
      </h4>
      <div className="content">
        {todos.map((t) => {
          return (
            <DraggableItem
              key={t.id}
              todo={t}
              onTodoDrag={onTodoDrag}
              onDragActive={onDragActive}
              onTodoDelete={onTodoDelete}
            />
          );
        })}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Draggable Item a.k.a Todo item

const DraggableItem = ({ todo, onTodoDrag, onDragActive, onTodoDelete }) => {
  const [{ isDragging }, drag] = useDrag(() => ({
    type: ITEM_TYPES.TODO,
    item: { todo },
    end: (item, monitor) => {
      const dropResult = monitor.getDropResult();
      if (item && dropResult) {
        if (!dropResult.delete) {
          onTodoDrag(item.todo.id, dropResult.state);
        } else {
          onTodoDelete(item.todo.id);
        }
        onDragActive(false);
      }
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
      handlerId: monitor.getHandlerId(),
    }),
    isDragging: (monitor) => {
      if (todo.id === monitor.getItem().todo.id) {
        onDragActive(true);
      }
    },
  }));
  const opacity = isDragging ? 0.4 : 1;
  const textDecoration =
    todo.state === TODO_STATE.DONE ? "line-through" : "none";
  let backgroundColor = "";
  switch (todo.state) {
    case TODO_STATE.NOT_STARTED: {
      backgroundColor = "lightcoral";
      break;
    }
    case TODO_STATE.IN_PROGRESS: {
      backgroundColor = "lightyellow";
      break;
    }
    case TODO_STATE.DONE: {
      backgroundColor = "lightgreen";
      break;
    }
    default: {
      backgroundColor = "white";
      break;
    }
  }
  return (
    <div
      className="draggable-item"
      ref={drag}
      style={{ opacity, textDecoration, backgroundColor }}
    >
      {todo.value}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Event : onTodoDrag

const onTodoDrag = (id, state) => {
    setTodos((_t) =>
      _t.map((t) => {
        if (t.id === id) {
          return { ...t, state };
        }
        return t;
      })
    );
 };
Enter fullscreen mode Exit fullscreen mode

Deleting todo

Delete Drop zone

const DeleteDragItemBox = () => {
  const [{ canDrop, isOver }, drop] = useDrop(() => ({
    accept: ITEM_TYPES.TODO,
    drop: () => ({ delete: true }),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  }));
  const isActive = canDrop && isOver;
  const style = {
    border: isActive ? "3px dashed black" : "none",
  };
  return (
    <div className="delete-drag-box" style={style} ref={drop}>
      <DeleteIcon width={'4rem'}/>
    </div>
  );
};

const onTodoDelete = (id) => {
    setTodos((_t) => _t.filter((t) => t.id !== id));
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing this game will allow you to learn about using state and side effects in React to implement the desired logic. This app was made as part of learning new components which are used in real life applications.
Stay safe and lend a hand to another :)

Top comments (1)

Collapse
stanzenkit profile image
Stan

Always interesting to have be able to go DIY Style !
We're fans of this subject and what gravitates around it, in our team. We wrote an article on the subject and we'd love your opinion: zenkit.com/en/blog/kanban-explaine...

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.