DEV Community

Cover image for How to Build a Beautiful Todo Application Using React JS and Custom CSS
Maniruzzaman Akash
Maniruzzaman Akash

Posted on

How to Build a Beautiful Todo Application Using React JS and Custom CSS

In this post, we'll walk through building a fully functional Todo application using React JS, custom CSS, and the SimpleCRUD API to handle backend operations like data fetching, pagination, creation, updates, and deletions.
The SimpleCRUD API simplifies backend complexities, letting us focus on building the UI and handling interactions effectively. Let's dive into the components of the application.


Setting Up the Environment

Required Libraries
Ensure you have the following libraries installed in your React project. These are already mentioned in your package.json:

  • React & React-DOM: For building the UI.
  • Axios: For making HTTP requests to the SimpleCRUD API.
  • React Icons: For icons to improve user experience.
  • date-fns: For formatting dates.

Project Structure

Here's how the project is structured - its using create-react-app.

/public
  /index.html
/src
  api.js
  App.js
  styles.css
  index.js
package.json
Enter fullscreen mode Exit fullscreen mode

Backend API Integration

The src/api.js file handles all the interactions with the SimpleCRUD API.

Simple CRUD API Docs for Todos - https://simplecrudapi.com/docs/todos

import axios from "axios";

const API_URL = "https://simplecrudapi.com/api/todos";

export const getTodos = async (params) => {
  let apiUrl = `${API_URL}?orderBy=id&orderDirection=desc`;
  const response = await axios.get(apiUrl, { params });
  const { data, meta } = response.data;
  return { data, meta };
};

export const createTodo = async (todo) => {
  todo.user_id = null;
  const response = await axios.post(API_URL, todo);
  return response.data.data;
};

export const updateTodo = async (id, todo) => {
  todo.user_id = null;
  const response = await axios.put(`${API_URL}/${id}`, todo);
  return response.data.data;
};

export const deleteTodo = async (id) => {
  const response = await axios.delete(`${API_URL}/${id}`);
  return response.data.data;
};
Enter fullscreen mode Exit fullscreen mode
  • getTodos: Fetches a paginated list of todos.
  • createTodo: Creates a new todo.
  • updateTodo: Updates an existing todo.
  • deleteTodo: Deletes a todo by its ID.
  1. Application Logic File: App.js This file contains the main application logic: State management for todos, form data, pagination, and editing. CRUD operations: Fetch, add, edit, and delete todos. Pagination controls: Navigate through pages of todos.

Walkthrough
Initial Setup
Use useState to manage the app's state, including todos and pagination details.
Use useEffect to fetch todos when the page loads or pagination state changes.

Todo Operations
Add a new todo or update an existing one using handleSubmit.
Delete a todo with handleDelete.
Edit a todo by populating the form fields with the selected todo's data.

Pagination
Handle pagination using changePage to navigate between pages.


Key Code Snippets

Fetching Todos

const fetchTodos = async (page = 1) => {
  try {
    const { data, meta } = await getTodos({ page });
    setTodos(data);
        setPagination((prev) => ({
          ...prev,
          currentPage: meta?.current_page || 1,
          lastPage: meta?.last_page || 1,
          total: meta?.total || 0,
        }));
      } catch (error) {
        console.error("Failed to fetch todos:", error);
        setTodos([]);
        setPagination((prev) => ({
          ...prev,
          currentPage: 1,
          lastPage: 1,
          total: 0,
        }));
      }
    };
Enter fullscreen mode Exit fullscreen mode

Handling Submit

const handleSubmit = async (e) => {
  e.preventDefault();
  if (editingId) {
    await updateTodo(editingId, form);
    setEditingId(null);
  } else {
    await createTodo(form);
  }

  setForm({ title: "", description: "", completed: false });
  fetchTodos(pagination.currentPage);
};
Enter fullscreen mode Exit fullscreen mode

Pagination

const changePage = (newPage) => {
  if (newPage > 0 && newPage <= pagination.lastPage) {
    setPagination((prev) => ({ ...prev, currentPage: newPage }));
  }
};
Enter fullscreen mode Exit fullscreen mode

Styling the Application

File: styles.css
The CSS file defines a sleek and modern look for the Todo app:
Animations: Subtle fadeIn and slideIn animations enhance user experience.

Responsiveness: Styles adapt well to various screen sizes.
Visual hierarchy: Clear distinction between buttons, input fields, and todo items.


Running the Application

Install Dependencies

npx create-react-app my-todo
cd my-todo
npm start
Enter fullscreen mode Exit fullscreen mode

Install Extra Dependencies

npm install axios react-icons date-fns
Enter fullscreen mode Exit fullscreen mode

Interact with the App

  • Add new todos using the form.
  • Edit existing todos by clicking the edit button.
  • Delete todos by clicking the delete button.
  • Navigate through pages of todos using the pagination controls.

Complete Source code

The src/App.js file -

import React, { useEffect, useState } from "react";
import { format } from "date-fns"; // Importing date-fns
import { getTodos, createTodo, updateTodo, deleteTodo } from "./api";
import { AiOutlineDelete, AiOutlineEdit } from "react-icons/ai";
import "./styles.css";

function App() {
  const [todos, setTodos] = useState([]);
  const [form, setForm] = useState({
    title: "",
    description: "",
    completed: false,
  });
  const [editingId, setEditingId] = useState(null);
  const [pagination, setPagination] = useState({
    currentPage: 1,
    lastPage: 1,
    perPage: 10,
    total: 0,
  });

  useEffect(() => {
    fetchTodos(pagination.currentPage);
  }, [pagination.currentPage]);

  const fetchTodos = async (page = 1) => {
    try {
      const { data, meta } = await getTodos({ page });

      setTodos(data);
      setPagination((prev) => ({
        ...prev,
        currentPage: meta?.current_page || 1,
        lastPage: meta?.last_page || 1,
        total: meta?.total || 0,
      }));
    } catch (error) {
      console.error("Failed to fetch todos:", error);
      setTodos([]);
      setPagination((prev) => ({
        ...prev,
        currentPage: 1,
        lastPage: 1,
        total: 0,
      }));
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (editingId) {
      await updateTodo(editingId, form);
      setEditingId(null);
    } else {
      await createTodo(form);
    }

    setForm({ title: "", description: "", completed: false });
    fetchTodos(pagination.currentPage);
  };

  const handleEdit = (todo) => {
    setEditingId(todo.id);
    setForm({
      title: todo.title,
      description: todo.description,
      completed: todo.completed,
    });
  };

  const handleDelete = async (id) => {
    await deleteTodo(id);
    fetchTodos(pagination.currentPage);
  };

  const changePage = (newPage) => {
    if (newPage > 0 && newPage <= pagination.lastPage) {
      setPagination((prev) => ({ ...prev, currentPage: newPage }));
    }
  };

  return (
    <div className="app-container">
      <div className="todo-wrapper">
        <h1 className="app-title">My Todos</h1>
        <p className="app-description">
          By Simple CRUD API →{" "}
          <a href="https://simplecrudapi.com/docs/todos">Todo API link</a>
        </p>

        {/* Form */}
        <form onSubmit={handleSubmit} className="todo-form">
          <input
            type="text"
            placeholder="Title"
            value={form.title}
            onChange={(e) => setForm({ ...form, title: e.target.value })}
            className="todo-input"
            required
          />
          <textarea
            placeholder="Description"
            value={form.description}
            onChange={(e) => setForm({ ...form, description: e.target.value })}
            className="todo-textarea"
            required
          ></textarea>
          <div className="todo-checkbox-wrapper">
            <input
              type="checkbox"
              checked={form.completed}
              onChange={(e) =>
                setForm({ ...form, completed: e.target.checked })
              }
              className="todo-checkbox"
            />
            <label>Completed</label>
          </div>
          <button type="submit" className="todo-button">
            {editingId ? "Update" : "Add"} Todo
          </button>
        </form>

        {/* Todo List */}
        <div className="todo-list">
          {todos.map((todo) => (
            <div key={todo.id} className="todo-item">
              <div>
                <h2
                  className={`todo-title ${todo.completed ? "completed" : ""}`}
                >
                  {todo.title}
                </h2>
                <p className="todo-description">{todo.description}</p>
                <p className="todo-updated-at">
                  Last updated:{" "}
                  {todo.updated_at
                    ? format(new Date(todo.updated_at), "MMM d, yyyy h:mm a")
                    : "N/A"}
                </p>
              </div>
              <div className="todo-actions">
                <button
                  onClick={() => handleEdit(todo)}
                  className="edit-button"
                >
                  <AiOutlineEdit />
                </button>
                <button
                  onClick={() => handleDelete(todo.id)}
                  className="delete-button"
                >
                  <AiOutlineDelete />
                </button>
              </div>
            </div>
          ))}
        </div>

        {/* Pagination */}
        <div className="pagination">
          <button
            className="pagination-button"
            onClick={() => changePage(pagination.currentPage - 1)}
            disabled={pagination.currentPage === 1}
          >
            &laquo; Previous
          </button>
          <span className="pagination-info">
            Page {pagination.currentPage} of {pagination.lastPage}
          </span>
          <button
            className="pagination-button"
            onClick={() => changePage(pagination.currentPage + 1)}
            disabled={pagination.currentPage === pagination.lastPage}
          >
            Next &raquo;
          </button>
        </div>
      </div>

      {/* Footer */}
      <footer className="app-footer">
        Powered by{" "}
        <a
          href="https://simplecrudapi.com"
          target="_blank"
          rel="noopener noreferrer"
        >
          simplecrudapi.com
        </a>
      </footer>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The src/styles.css file -

/* General App Styles */
.app-container {
  font-family: "Roboto", Arial, sans-serif;
  background: linear-gradient(135deg, #f0f8ff, #e0e7ff);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  box-sizing: border-box;
}

.todo-wrapper {
  max-width: 650px;
  width: 100%;
  background: #fff;
  padding: 25px;
  border-radius: 12px;
  box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
  animation: fadeIn 0.8s ease-in-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.app-title {
  font-size: 28px;
  font-weight: bold;
  text-align: center;
  color: #2c3e50;
  margin-bottom: 12px;
}

.app-description {
  text-align: center;
  font-size: 14px;
  margin-bottom: 25px;
  color: #34495e;
}

.app-description a {
  color: #3498db;
  text-decoration: none;
  font-weight: 500;
}

.app-description a:hover {
  text-decoration: underline;
}

/* Form Styles */
.todo-form {
  margin-bottom: 25px;
}

.todo-input,
.todo-textarea {
  width: 100%;
  padding: 12px;
  margin-bottom: 12px;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  font-size: 14px;
  box-sizing: border-box;
  transition: border-color 0.3s ease;
}

.todo-input:focus,
.todo-textarea:focus {
  outline: none;
  border-color: #007bff;
}

.todo-textarea {
  height: 90px;
  resize: none;
}

.todo-checkbox-wrapper {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}

.todo-checkbox {
  margin-right: 8px;
}

.todo-button {
  display: block;
  width: 100%;
  padding: 12px;
  background: #4caf50;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  transition: all 0.3s ease;
}

.todo-button:hover {
  background: #45a049;
  transform: translateY(-2px);
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
}

/* Todo List Styles */
.todo-list {
  margin-top: 20px;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: #fdfdfd;
  padding: 15px 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin-bottom: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  transition: all 0.3s ease;
  animation: slideIn 0.5s ease forwards;
  overflow-x: hidden;
  gap: 10px;
}

.todo-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.todo-title {
  font-size: 18px;
  font-weight: bold;
  margin: 0;
  color: #2c3e50;
  word-wrap: break-word;
  word-break: break-word;
  overflow-wrap: break-word;
}

.todo-title.completed {
  text-decoration: line-through;
  color: #7f8c8d;
}

.todo-description {
  font-size: 15px;
  color: #95a5a6;
  margin: 5px 0 0;
  word-wrap: break-word;
  word-break: break-word;
  overflow-wrap: break-word;
}

.todo-actions {
  display: flex;
  gap: 10px;
}

.edit-button,
.delete-button {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 20px;
  transition: all 0.3s ease;
}

.edit-button {
  color: #3498db;
}

.edit-button:hover {
  color: #2980b9;
  transform: scale(1.1);
}

.delete-button {
  color: #e74c3c;
}

.delete-button:hover {
  color: #c0392b;
  transform: scale(1.1);
}

/* Pagination Styles */
.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 15px;
  margin-top: 20px;
}

.pagination-button {
  background: #3498db;
  color: white;
  padding: 10px 15px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
}

.pagination-button:hover {
  background: #2980b9;
  transform: translateY(-2px);
}

.pagination-button:disabled {
  background: #95a5a6;
  cursor: not-allowed;
}

.pagination-info {
  font-size: 14px;
  color: #2c3e50;
}

/* Footer Styles */
.app-footer {
  margin-top: 30px;
  text-align: center;
  font-size: 14px;
  color: #555;
}

.app-footer a {
  color: #3498db;
  text-decoration: none;
}

.app-footer a:hover {
  text-decoration: underline;
}

.todo-updated-at {
  font-size: 12px;
  color: #7f8c8d;
  margin-top: 5px;
}
Enter fullscreen mode Exit fullscreen mode

In package.json -

{
  "name": "react",
  "version": "1.0.0",
  "description": "",
  "keywords": [],
  "main": "src/index.tsx",
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "react-scripts": "^5.0.0",
    "axios": "1.7.7",
    "react-icons": "5.3.0",
    "date-fns": "4.1.0"
  },
  "devDependencies": {
    "@types/react": "18.2.38",
    "@types/react-dom": "18.2.15",
    "loader-utils": "3.2.1",
    "typescript": "4.4.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}
Enter fullscreen mode Exit fullscreen mode

UI Demo

Image description

Summary

This Todo app demonstrates the seamless integration of React, custom CSS, and a backend API (SimpleCRUD API). The design is clean, and the functionality covers all essential aspects of a task management application, making it a perfect starter project for React enthusiasts.

For more information on the SimpleCRUD API, refer to their documentation - https://simplecrudapi.com/docs/todos

Top comments (0)