✨ What I Built
A clean and functional Todo App using React.js - mainly built to practice component structure, state management, and dark/light theming.
This version is desktop-only for now, but I plan to make it fully responsive later.
⚙️ Features
✅ Add new tasks
❌ Delete tasks
🕹️ Toggle task completion
🌗 Dark/Light mode toggle
🔍 Filter tasks by:
🔹All
🔷Active
✅Completed
🧹 Clear all completed tasks
🔄 Real-time updates using useState and props
🔧 Tech Stack
React.js (Functional Components + Hooks)
CSS Modules for scoped styling
No external UI libraries
Modular file structure
🎯 What I Practiced
Lifting and managing state between components
Controlled form elements
Dynamic filtering
Class toggling for theme switch
Code reusability (e.g., component)
🖼️ Screenshots
- Main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
- App.jsx
import "./App.css";
import { useState } from "react";
import TodoHeader from "./components/TodoHeader";
import TodoForm from "./components/TodoForm";
import TodoList from "./components/TodoList";
function App() {
const [isDarkMode, setIsDarkMode] = useState(false);
const [todoItems, setTodoItems] = useState([]);
const handleAddTodoItems = (item) => {
setTodoItems((items) => [...items, item]);
};
const handleDeleteItems = (id) => {
setTodoItems((items) => items.filter((item) => item.id !== id));
};
const handleToggleComplete = (id) => {
setTodoItems((items) =>
items.map((item) => (item.id === id ? { ...item, completed: !item.completed } : item))
);
};
const handleClearCompleted = () => {
setTodoItems((items) => items.filter((item) => !item.completed));
};
return (
<>
<main className={`todo-app ${isDarkMode ? "dark" : "light"}`}>
<section className="todo-app-section">
<TodoHeader onChangeTheme={setIsDarkMode} isDarkMode={isDarkMode} />
<TodoForm onAddTodoItems={handleAddTodoItems} />
{todoItems.length > 0 ? (
<TodoList
todoItems={todoItems}
setTodoItems={setTodoItems}
onTaskComplete={handleToggleComplete}
onDeleteItem={handleDeleteItems}
onClearCompleted={handleClearCompleted}
/>
) : (
""
)}
</section>
</main>
</>
);
}
export default App;
- TodoHeader.jsx
import "../styles/Header.css";
import Button from "./ui/Button";
function TodoHeader({ isDarkMode, onChangeTheme }) {
const handleThemeToggler = () => {
onChangeTheme((isDarkMode) => !isDarkMode);
};
return (
<header className="todo-header">
<h1>Todo</h1>
<Button className={"theme-toggle"} onClick={handleThemeToggler}>
<img src={`icon-${isDarkMode ? "sun.svg" : "moon.svg"}`} alt="icon" />
</Button>
</header>
);
}
export default TodoHeader;
- UI Button.jsx
function Button({ className, onClick, children, id }) {
return (
<button className={className} onClick={onClick} id={id}>
{children}
</button>
);
}
export default Button;
- TodoForm.jsx
import { useState } from "react";
import "../styles/TodoForm.css";
import Button from "./ui/Button";
function TodoForm({ onAddTodoItems }) {
const [task, setTask] = useState("");
const handleForm = (e) => {
e.preventDefault();
if (!task.trim()) return;
const newTask = {
id: Date.now(),
task: task,
completed: false,
};
onAddTodoItems(newTask);
setTask("");
};
return (
<div className="todo-form-wrapper">
<form action="" className="todo-form" onSubmit={handleForm}>
<Button className={"todo-submit"}></Button>
<label htmlFor="todo-input" className="todo-label"></label>
<input
type="text"
name="todo-input"
id="todo-input"
placeholder="Create a new todo..."
onChange={(e) => setTask(e.target.value)}
value={task}
/>
</form>
</div>
);
}
export default TodoForm;
- TodoList.jsx
import TodoItem from "./TodoItem";
import "../styles/TodoList.css";
import TodoFooter from "./TodoFooter";
import { useState } from "react";
function TodoList({ todoItems, setTodoItems, onTaskComplete, onDeleteItem, onClearCompleted }) {
const [activeTab, setActiveTab] = useState("all");
let filterItems;
if (activeTab === "all") filterItems = todoItems;
else if (activeTab === "active")
filterItems = todoItems.filter((item) => item.completed === false);
else filterItems = todoItems.filter((item) => item.completed === true);
return (
<>
<ul className="todo-items">
{filterItems.map((item) => {
return (
<TodoItem
item={item}
key={item.id}
setTodoItems={setTodoItems}
onTaskComplete={onTaskComplete}
onDeleteItem={onDeleteItem}
/>
);
})}
</ul>
<TodoFooter
filter={activeTab}
onSetFilter={setActiveTab}
todoItems={todoItems}
onClearCompleted={onClearCompleted}
/>
</>
);
}
export default TodoList;
- TodoItem.jsx
import Button from "./ui/Button";
function TodoItem({ item, onTaskComplete, onDeleteItem }) {
return (
<li className="todo-item">
<input
type="checkbox"
name="todo-checkbox"
id={`todo-checkbox-${item.id}`}
className="todo-checkbox"
checked={item.completed}
onChange={() => onTaskComplete(item.id)}
/>
<label htmlFor={`todo-checkbox-${item.id}`} className="todo-checkbox-wrapper">
<img src="icon-check.svg" alt="check" />
</label>
<span className="todo-text">{item.task}</span>
<Button className="delete-item" onClick={() => onDeleteItem(item.id)}>
<img src="icon-cross.svg" alt="x" />
</Button>
</li>
);
}
export default TodoItem;
- TodoFooter.jsx
import Button from "./ui/Button";
import "../styles/TodoFooter.css";
const ctaBtnText = ["All", "Active", "Completed"];
function TodoFooter({ filter, onSetFilter, todoItems, onClearCompleted }) {
const activeTodoItems = todoItems.filter((item) => !item.completed).length;
return (
<div className="todo-footer">
<p className="todo-item-count">{activeTodoItems} items left</p>
<div className="todo-footer-cta">
{ctaBtnText.map((btn) => (
<Button
className={`cta-action-btn`}
key={btn}
onClick={() => onSetFilter(btn.toLocaleLowerCase())}
>
<span
className={`${
filter === btn.toLowerCase() ? "cta-action-btn--active" : ""
}`}
>
{btn}
</span>
</Button>
))}
</div>
<Button className={"cta-action-btn"} onClick={onClearCompleted}>
<span>Clear Completed</span>
</Button>
</div>
);
}
export default TodoFooter;
Let me know what you think or if you have any feedback! Cheers! 🚀
Top comments (0)