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
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;
};
- getTodos: Fetches a paginated list of todos.
- createTodo: Creates a new todo.
- updateTodo: Updates an existing todo.
- deleteTodo: Deletes a todo by its ID.
- 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,
}));
}
};
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);
};
Pagination
const changePage = (newPage) => {
if (newPage > 0 && newPage <= pagination.lastPage) {
setPagination((prev) => ({ ...prev, currentPage: newPage }));
}
};
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
Install Extra Dependencies
npm install axios react-icons date-fns
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}
>
« 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 »
</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;
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;
}
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"
]
}
UI Demo
- TODO App Demo8. Live Sandbox Demo Link
- Live App Link - https://lyjxd2.csb.app/
- Code Sandbox Code Editor - https://codesandbox.io/p/sandbox/lyjxd2
- All Sandbox Demos of SimpleCRUD API - https://simplecrudapi.com/sandbox-demo
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)