Initial commit: Todo app with vanilla JS and localStorage

- Single-file HTML app with embedded CSS and JS
- Add, complete, and delete todos
- Filter by All/Active/Completed
- Persistent storage via localStorage
- Responsive design with clean UI
- README with usage instructions
This commit is contained in:
Conduit Agent 2026-03-17 15:06:03 +00:00
commit 00f8859715
2 changed files with 471 additions and 0 deletions

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# ✅ Todo App
A clean, minimal todo application built with **vanilla JavaScript**, **HTML**, and **CSS** — no frameworks, no dependencies, no build tools.
![Made with HTML](https://img.shields.io/badge/Made%20with-HTML%2FJS%2FCSS-blue)
![No Dependencies](https://img.shields.io/badge/Dependencies-Zero-brightgreen)
![Storage](https://img.shields.io/badge/Storage-localStorage-orange)
## ✨ Features
- **Add todos** — type and press Enter or click Add
- **Complete todos** — click the circle checkbox to toggle
- **Delete todos** — hover and click the ✕ button
- **Filter todos** — view All, Active, or Completed
- **Clear completed** — bulk remove finished tasks
- **Persistent storage** — todos survive page refreshes via `localStorage`
- **Live stats** — see total, active, and completed counts
- **Responsive design** — works on desktop and mobile
- **Keyboard friendly** — press Enter to quickly add todos
## 🚀 Getting Started
No build step required! Just open the file:
```bash
# Option 1: Open directly in your browser
open index.html
# Option 2: Serve with Python
python3 -m http.server 8080
# Option 3: Serve with Node.js
npx serve .
```
Then visit `http://localhost:8080` (for options 2 & 3).
## 📁 Project Structure
```
todo-app/
├── index.html # The entire app — HTML, CSS, and JS in one file
└── README.md # You're reading it
```
## 🛠️ Tech Stack
| Tech | Purpose |
|------|---------|
| HTML5 | Structure |
| CSS3 | Styling with gradients, animations, and responsive layout |
| Vanilla JS | App logic, DOM manipulation, event handling |
| localStorage | Client-side data persistence |
## 💡 How It Works
1. Todos are stored as a JSON array in `localStorage`
2. Each todo has an `id` (timestamp), `text`, `completed` flag, and `createdAt` date
3. The `render()` function re-draws the list based on current state and active filter
4. All mutations (add, toggle, delete, clear) update state → save to localStorage → re-render
## 📝 License
MIT — do whatever you want with it.

407
index.html Normal file
View File

@ -0,0 +1,407 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>✅ Todo App</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px 20px;
}
.container {
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 520px;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px;
text-align: center;
}
.header h1 {
color: #fff;
font-size: 1.8rem;
font-weight: 700;
letter-spacing: 1px;
}
.header p {
color: rgba(255, 255, 255, 0.8);
margin-top: 6px;
font-size: 0.9rem;
}
.input-section {
display: flex;
padding: 20px;
gap: 10px;
border-bottom: 1px solid #eee;
}
.input-section input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
.input-section input:focus {
border-color: #667eea;
}
.input-section button {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.input-section button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.input-section button:active {
transform: translateY(0);
}
.stats {
display: flex;
justify-content: space-around;
padding: 14px 20px;
background: #f8f9fa;
border-bottom: 1px solid #eee;
font-size: 0.85rem;
color: #666;
}
.stats span {
font-weight: 700;
color: #667eea;
}
.filters {
display: flex;
justify-content: center;
gap: 8px;
padding: 14px 20px;
border-bottom: 1px solid #eee;
}
.filters button {
padding: 6px 16px;
border: 2px solid #e0e0e0;
border-radius: 20px;
background: #fff;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
color: #555;
}
.filters button.active {
border-color: #667eea;
background: #667eea;
color: #fff;
}
.filters button:hover:not(.active) {
border-color: #667eea;
color: #667eea;
}
.todo-list {
list-style: none;
padding: 10px 20px 20px;
min-height: 80px;
}
.todo-list .empty-state {
text-align: center;
padding: 40px 20px;
color: #bbb;
font-size: 1rem;
}
.todo-item {
display: flex;
align-items: center;
padding: 14px 12px;
margin: 6px 0;
border-radius: 10px;
background: #f8f9fa;
transition: all 0.2s;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.todo-item:hover {
background: #eef0ff;
}
.todo-item .checkbox {
width: 22px;
height: 22px;
border: 2px solid #ccc;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
}
.todo-item .checkbox:hover {
border-color: #667eea;
}
.todo-item.completed .checkbox {
background: #667eea;
border-color: #667eea;
}
.todo-item.completed .checkbox::after {
content: '✓';
color: #fff;
font-size: 14px;
font-weight: 700;
}
.todo-item .text {
flex: 1;
margin: 0 14px;
font-size: 0.95rem;
color: #333;
word-break: break-word;
}
.todo-item.completed .text {
text-decoration: line-through;
color: #aaa;
}
.todo-item .delete-btn {
width: 30px;
height: 30px;
border: none;
background: transparent;
color: #ccc;
font-size: 1.2rem;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.todo-item .delete-btn:hover {
background: #ffe0e0;
color: #e74c3c;
}
.footer {
padding: 14px 20px;
text-align: center;
border-top: 1px solid #eee;
}
.footer button {
padding: 8px 20px;
background: transparent;
border: 2px solid #e74c3c;
color: #e74c3c;
border-radius: 20px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.footer button:hover {
background: #e74c3c;
color: #fff;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>✅ Todo App</h1>
<p>Stay organized, get things done</p>
</div>
<div class="input-section">
<input type="text" id="todoInput" placeholder="What needs to be done?" autofocus>
<button onclick="addTodo()">Add</button>
</div>
<div class="stats" id="stats">
<div>Total: <span id="totalCount">0</span></div>
<div>Active: <span id="activeCount">0</span></div>
<div>Completed: <span id="completedCount">0</span></div>
</div>
<div class="filters">
<button class="active" onclick="setFilter('all', this)">All</button>
<button onclick="setFilter('active', this)">Active</button>
<button onclick="setFilter('completed', this)">Completed</button>
</div>
<ul class="todo-list" id="todoList"></ul>
<div class="footer" id="footer" style="display:none;">
<button onclick="clearCompleted()">🗑️ Clear Completed</button>
</div>
</div>
<script>
// ── State ──────────────────────────────────────────
let todos = JSON.parse(localStorage.getItem('todos')) || [];
let currentFilter = 'all';
// ── Persistence ────────────────────────────────────
function save() {
localStorage.setItem('todos', JSON.stringify(todos));
}
// ── Add ────────────────────────────────────────────
function addTodo() {
const input = document.getElementById('todoInput');
const text = input.value.trim();
if (!text) return;
todos.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
});
save();
render();
input.value = '';
input.focus();
}
// ── Toggle Complete ────────────────────────────────
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
save();
render();
}
// ── Delete ─────────────────────────────────────────
function deleteTodo(id) {
todos = todos.filter(t => t.id !== id);
save();
render();
}
// ── Clear Completed ────────────────────────────────
function clearCompleted() {
todos = todos.filter(t => !t.completed);
save();
render();
}
// ── Filter ─────────────────────────────────────────
function setFilter(filter, btn) {
currentFilter = filter;
document.querySelectorAll('.filters button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
render();
}
// ── Render ─────────────────────────────────────────
function render() {
const list = document.getElementById('todoList');
const filtered = todos.filter(t => {
if (currentFilter === 'active') return !t.completed;
if (currentFilter === 'completed') return t.completed;
return true;
});
if (filtered.length === 0) {
list.innerHTML = `<div class="empty-state">${
todos.length === 0
? '🎯 Add your first todo above!'
: 'No todos match this filter.'
}</div>`;
} else {
list.innerHTML = filtered.map(todo => `
<li class="todo-item ${todo.completed ? 'completed' : ''}">
<div class="checkbox" onclick="toggleTodo(${todo.id})"></div>
<span class="text">${escapeHTML(todo.text)}</span>
<button class="delete-btn" onclick="deleteTodo(${todo.id})" title="Delete"></button>
</li>
`).join('');
}
// Update stats
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
const active = total - completed;
document.getElementById('totalCount').textContent = total;
document.getElementById('activeCount').textContent = active;
document.getElementById('completedCount').textContent = completed;
// Show/hide clear button
document.getElementById('footer').style.display = completed > 0 ? 'block' : 'none';
}
// ── Helpers ────────────────────────────────────────
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── Keyboard Support ───────────────────────────────
document.getElementById('todoInput').addEventListener('keydown', e => {
if (e.key === 'Enter') addTodo();
});
// ── Initial Render ─────────────────────────────────
render();
</script>
</body>
</html>