- 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
407 lines
10 KiB
HTML
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> |