Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# Todo
# DevByRico’s Todo App

A clean, minimal and responsive Todo application built with React and Zustand for global state management.
Track your tasks by adding, toggling complete/incomplete, and removing them.

---

## 🚀 Live Demo

Check it out here 👉 [https://todo-rn.netlify.app](https://todo-rn.netlify.app)

---

## ⚙️ Features

- **Add Tasks**: Quickly add new todos via a simple form.
- **Toggle Complete**: Mark tasks as done/undone with a checkbox.
- **Remove Tasks**: Delete tasks you no longer need.
- **Global State**: Powered by Zustand—no prop drilling.
- **Responsive Design**: Works beautifully from mobile (320px) to desktop.
- **Modern UI**: Soft shadows, smooth hover effects and clean typography.

---

## 📂 Project Structure

```text
devbyrico-todo-zustand/
├── public/
│ └── index.html # Main HTML file
└── src/
├── components/
│ ├── AddTodo.jsx # Form to add a new task
│ └── TodoList.jsx # Renders list of todos
├── store/
│ └── todoStore.js # Zustand store with add/toggle/remove logic
├── App.jsx # Main layout and component imports
├── App.css # Component-level CSS
├── index.js # ReactDOM entry point
└── index.css # Global resets & base styles
├── package.json # Project metadata & scripts
└── README.md # Project documentation
44 changes: 23 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
{
"name": "todo",
"name": "ricardo-todo-zustand",
"version": "1.0.0",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"zustand": "^4.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"vite": "^6.2.0"
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
14 changes: 14 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ricardo Todo</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
105 changes: 105 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
.App {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
max-width: 600px;
margin: auto;
}

.App h1 {
margin-bottom: 1.5rem;
font-size: 2rem;
letter-spacing: 0.05em;
}

form {
display: flex;
width: 100%;
margin-bottom: 2rem;
}

form input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid #ccd0d5;
border-radius: 0.5rem 0 0 0.5rem;
font-size: 1rem;
}

form button {
padding: 0.75rem 1.5rem;
border: none;
background-color: #4a90e2;
color: white;
font-size: 1rem;
cursor: pointer;
border-radius: 0 0.5rem 0.5rem 0;
transition: background-color 0.2s ease;
}

form button:hover {
background-color: #3a78c2;
}

ul {
list-style: none;
width: 100%;
}

li {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
margin-bottom: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: transform 0.1s ease;
}

li:hover {
transform: translateY(-2px);
}

li input[type="checkbox"] {
margin-right: 1rem;
width: 1.2rem;
height: 1.2rem;
}

li span {
flex: 1;
font-size: 1rem;
}

li button {
border: none;
background: transparent;
font-size: 1.2rem;
color: #e74c3c;
cursor: pointer;
transition: color 0.2s ease;
}

li button:hover {
color: #c0392b;
}

@media (max-width: 400px) {
.App {
padding: 1rem;
}
form input, form button {
font-size: 0.9rem;
}
li {
flex-direction: column;
align-items: flex-start;
}
li button {
align-self: flex-end;
margin-top: 0.5rem;
}
}
18 changes: 15 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
export const App = () => {

import React from "react";
import "./App.css";
import { TodoList } from "./components/TodoList";
import { AddTodo } from "./components/AddTodo";

function App() {
return (
<h1>React Boilerplate</h1>
)
<div className="App">
<h1>Ricardo's Todo App</h1>
<AddTodo />
<TodoList />
</div>
);
}

export default App;
27 changes: 27 additions & 0 deletions src/components/AddTodo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

import React, { useState } from "react";
import { useTodoStore } from "../store/todoStore";

export const AddTodo = () => {
const [input, setInput] = useState("");
const addTodo = useTodoStore((state) => state.addTodo);

const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
addTodo(input);
setInput("");
}
};

return (
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add</button>
</form>
);
};
25 changes: 25 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

import React from "react";
import { useTodoStore } from "../store/todoStore";

export const TodoList = () => {
const { todos, toggleTodo, removeTodo } = useTodoStore();

return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? "line-through" : "" }}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>❌</button>
</li>
))}
</ul>
);
};
13 changes: 11 additions & 2 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

html, body, #root {
height: 100%;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: #f5f7fa;
color: #333;
}
8 changes: 8 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
23 changes: 23 additions & 0 deletions src/store/todoStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

import { create } from "zustand";

export const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [
...state.todos,
{ id: Date.now(), text, completed: false, createdAt: new Date() },
],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
}));