Skip to content

Commit a3d3622

Browse files
committed
Add ToDo List app (HTML, CSS, JS) with localStorage persistence
1 parent 76362b2 commit a3d3622

3 files changed

Lines changed: 238 additions & 0 deletions

File tree

root/assets/css/todo-list.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
:root{
2+
--bg: #f5f7fb;
3+
--card: #fff;
4+
--accent: #5c6ac4;
5+
--muted: #6b7280;
6+
--success: #10b981;
7+
}
8+
*{box-sizing:border-box}
9+
body{font-family:Inter,system-ui,Segoe UI,Roboto,Arial,sans-serif;background:var(--bg);margin:0;color:#0f172a}
10+
.todo-app{max-width:720px;margin:48px auto;padding:20px}
11+
.todo-header h1{margin:0 0 4px;font-size:28px}
12+
.subtitle{margin:0;color:var(--muted);font-size:13px}
13+
.todo-input-wrap{margin-top:18px}
14+
.todo-form{display:flex;gap:8px}
15+
.todo-form input{flex:1;padding:12px 14px;border:1px solid #e6e9f0;border-radius:8px;font-size:15px}
16+
.todo-form button{background:var(--accent);color:#fff;border:0;padding:0 14px;border-radius:8px;font-weight:600;cursor:pointer}
17+
.todo-controls{display:flex;justify-content:space-between;align-items:center;margin:14px 0}
18+
.filters{display:flex;gap:8px}
19+
.filter{background:transparent;border:1px solid transparent;padding:6px 10px;border-radius:6px;cursor:pointer;color:var(--muted)}
20+
.filter.active{background:var(--card);border-color:#e2e8f0;color:var(--accent)}
21+
.actions button{background:transparent;border:0;color:var(--muted);cursor:pointer}
22+
.todo-list-wrap{background:transparent}
23+
.todo-list{list-style:none;margin:0;padding:0;border-radius:8px}
24+
.todo-item{display:flex;align-items:center;gap:12px;padding:12px;background:var(--card);border:1px solid #eef2ff;margin-bottom:8px;border-radius:8px}
25+
.todo-item .left{display:flex;align-items:center;gap:10px;flex:1}
26+
.todo-item input[type="checkbox"]{width:18px;height:18px}
27+
.task-text{font-size:15px}
28+
.task-text.completed{text-decoration:line-through;color:var(--muted)}
29+
.todo-item .actions{display:flex;gap:8px}
30+
.todo-item button{background:transparent;border:0;color:var(--muted);cursor:pointer}
31+
.todo-footer{margin-top:14px;color:var(--muted);font-size:14px}
32+
33+
@media (max-width:520px){.todo-app{padding:12px;margin:20px}}
34+
35+
/* small utility for editing */
36+
.edit-input{width:100%;padding:8px;border-radius:6px;border:1px solid #e6e9f0}
37+
38+
/* focus styles */
39+
input:focus,button:focus{outline:2px solid rgba(92,106,196,.12);outline-offset:2px}

root/assets/scripts/todo-list.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
(() => {
2+
const STORAGE_KEY = 'todo-list-tasks-v1';
3+
4+
const form = document.getElementById('todo-form');
5+
const input = document.getElementById('todo-input');
6+
const listEl = document.getElementById('todo-list');
7+
const itemsLeft = document.getElementById('items-left');
8+
const filters = document.querySelectorAll('.filter');
9+
const clearCompletedBtn = document.getElementById('clear-completed');
10+
11+
let tasks = [];
12+
let filter = 'all';
13+
14+
function save() {
15+
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
16+
}
17+
18+
function load() {
19+
try {
20+
const raw = localStorage.getItem(STORAGE_KEY);
21+
tasks = raw ? JSON.parse(raw) : [];
22+
} catch (e) {
23+
tasks = [];
24+
}
25+
}
26+
27+
function uid() {
28+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
29+
}
30+
31+
function render() {
32+
listEl.innerHTML = '';
33+
const visible = tasks.filter(t => {
34+
if (filter === 'active') return !t.completed;
35+
if (filter === 'completed') return t.completed;
36+
return true;
37+
});
38+
39+
visible.forEach(task => listEl.appendChild(createTaskEl(task)));
40+
itemsLeft.textContent = tasks.filter(t => !t.completed).length;
41+
}
42+
43+
function createTaskEl(task) {
44+
const li = document.createElement('li');
45+
li.className = 'todo-item';
46+
li.dataset.id = task.id;
47+
48+
const left = document.createElement('div');
49+
left.className = 'left';
50+
51+
const chk = document.createElement('input');
52+
chk.type = 'checkbox';
53+
chk.checked = !!task.completed;
54+
chk.addEventListener('change', () => {
55+
task.completed = chk.checked;
56+
save();
57+
render();
58+
});
59+
60+
const span = document.createElement('span');
61+
span.className = 'task-text' + (task.completed ? ' completed' : '');
62+
span.textContent = task.text;
63+
span.title = 'Double click to edit';
64+
span.addEventListener('dblclick', () => enterEditMode(li, task, span));
65+
66+
left.appendChild(chk);
67+
left.appendChild(span);
68+
69+
const actions = document.createElement('div');
70+
actions.className = 'actions';
71+
72+
const editBtn = document.createElement('button');
73+
editBtn.type = 'button';
74+
editBtn.textContent = 'Edit';
75+
editBtn.addEventListener('click', () => enterEditMode(li, task, span));
76+
77+
const delBtn = document.createElement('button');
78+
delBtn.type = 'button';
79+
delBtn.textContent = 'Delete';
80+
delBtn.addEventListener('click', () => {
81+
tasks = tasks.filter(t => t.id !== task.id);
82+
save();
83+
render();
84+
});
85+
86+
actions.appendChild(editBtn);
87+
actions.appendChild(delBtn);
88+
89+
li.appendChild(left);
90+
li.appendChild(actions);
91+
return li;
92+
}
93+
94+
function enterEditMode(li, task, span) {
95+
const input = document.createElement('input');
96+
input.type = 'text';
97+
input.className = 'edit-input';
98+
input.value = task.text;
99+
span.replaceWith(input);
100+
input.focus();
101+
input.select();
102+
103+
function finish(saveText) {
104+
if (saveText) {
105+
const text = input.value.trim();
106+
if (text) task.text = text;
107+
}
108+
save();
109+
render();
110+
}
111+
112+
input.addEventListener('blur', () => finish(true));
113+
input.addEventListener('keydown', (e) => {
114+
if (e.key === 'Enter') {
115+
e.preventDefault();
116+
input.blur();
117+
} else if (e.key === 'Escape') {
118+
finish(false);
119+
}
120+
});
121+
}
122+
123+
form.addEventListener('submit', (e) => {
124+
e.preventDefault();
125+
const text = input.value.trim();
126+
if (!text) return;
127+
const task = { id: uid(), text, completed: false };
128+
tasks.unshift(task);
129+
save();
130+
input.value = '';
131+
render();
132+
});
133+
134+
filters.forEach(btn => btn.addEventListener('click', () => {
135+
filters.forEach(b => b.classList.remove('active'));
136+
btn.classList.add('active');
137+
filter = btn.dataset.filter;
138+
render();
139+
}));
140+
141+
clearCompletedBtn.addEventListener('click', () => {
142+
tasks = tasks.filter(t => !t.completed);
143+
save();
144+
render();
145+
});
146+
147+
// initial load
148+
load();
149+
render();
150+
151+
// expose for debugging (optional)
152+
window.__todo = { get tasks() { return tasks; }, save };
153+
154+
})();

root/pages/todo-list.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>ToDo List</title>
7+
<link rel="stylesheet" href="../assets/css/todo-list.css" />
8+
</head>
9+
<body>
10+
<main class="todo-app">
11+
<header class="todo-header">
12+
<h1>ToDo List</h1>
13+
<p class="subtitle">Simple, local, and fast — built with plain JavaScript</p>
14+
</header>
15+
16+
<section class="todo-input-wrap">
17+
<form id="todo-form" class="todo-form" autocomplete="off">
18+
<input id="todo-input" type="text" name="todo" placeholder="Add a new task..." />
19+
<button id="add-btn" type="submit">Add</button>
20+
</form>
21+
</section>
22+
23+
<section class="todo-controls">
24+
<div class="filters">
25+
<button data-filter="all" class="filter active">All</button>
26+
<button data-filter="active" class="filter">Active</button>
27+
<button data-filter="completed" class="filter">Completed</button>
28+
</div>
29+
<div class="actions">
30+
<button id="clear-completed">Clear completed</button>
31+
</div>
32+
</section>
33+
34+
<section class="todo-list-wrap">
35+
<ul id="todo-list" class="todo-list" aria-live="polite"></ul>
36+
</section>
37+
38+
<footer class="todo-footer">
39+
<span id="items-left">0</span> items left
40+
</footer>
41+
</main>
42+
43+
<script src="../assets/scripts/todo-list.js"></script>
44+
</body>
45+
</html>

0 commit comments

Comments
 (0)