todo-app/index.html
Conduit Agent 1f4ba296e9 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
2026-03-17 15:06:28 +00:00

407 lines
10 KiB
HTML

<!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>