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:
commit
00f8859715
64
README.md
Normal file
64
README.md
Normal 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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ 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
407
index.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user