Todo List
Фінальний проєкт блоку JavaScript & DOM. Ти створиш повноцінний Todo List, який об'єднає все, що ти вивчив: DOM-маніпуляції, обробку подій, роботу з формами та localStorage.
Файлова структура
todo-list/
├── index.html
├── style.css
└── script.js
Загальний вигляд
┌──────────── Todo List ────────────┐
│ │
│ ┌─────────────────────┐ [Додати] │
│ │ Що потрібно зробити? │ │
│ └─────────────────────┘ │
│ │
│ [Всі] [Активні] [Виконані] │
│ │
│ ☐ Купити продукти [✕] │
│ ☑ Прочитати книгу [✕] │
│ ☐ Зробити домашку [✕] │
│ ☐ Подзвонити другу [✕] │
│ │
│ 3 завдання залишилось │
│ [Очистити] │
└───────────────────────────────────┘
Частина 1: Додавання задач
Вимоги
1. Поле введення та кнопка:
<div class="todo-input">
<input type="text" id="todo-input" placeholder="Що потрібно зробити?">
<button id="todo-add">Додати</button>
</div>
2. Додавання задачі:
- Клік на кнопку "Додати" або натискання
Enter— створити нову задачу - Порожній ввід ігнорується (не створювати порожні задачі)
- Після додавання поле очищується та отримує фокус
3. Структура даних:
// Кожна задача — об'єкт
const todo = {
id: Date.now(), // унікальний ID
text: 'Купити продукти', // текст задачі
completed: false, // статус
createdAt: new Date().toISOString(),
};
// Усі задачі зберігаються в масиві
let todos = [];
4. Рендеринг списку:
function renderTodos() {
const list = document.getElementById('todo-list');
const filtered = getFilteredTodos();
list.innerHTML = '';
filtered.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
<span class="todo-text">${todo.text}</span>
<button class="todo-delete">×</button>
`;
list.appendChild(li);
});
updateCounter();
saveTodos();
}
Частина 2: Позначення виконаних
Вимоги
- Клік на чекбокс або на текст задачі — toggle стану
completed - Виконані задачі мають перекреслений текст та напівпрозорість
.todo-item.completed .todo-text {
text-decoration: line-through;
opacity: 0.5;
}
// Використай делегування подій
list.addEventListener('click', (e) => {
const item = e.target.closest('.todo-item');
if (!item) return;
const id = Number(item.dataset.id);
if (e.target.classList.contains('todo-checkbox') || e.target.classList.contains('todo-text')) {
toggleTodo(id);
}
if (e.target.classList.contains('todo-delete')) {
deleteTodo(id);
}
});
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
renderTodos();
}
}
Частина 3: Видалення задач
Вимоги
- Кнопка
xна кожній задачі видаляє її - Задача видаляється з масиву
todosта перерендерюється список
function deleteTodo(id) {
todos = todos.filter(t => t.id !== id);
renderTodos();
}
Частина 4: Редагування задач
Вимоги
- Подвійний клік (
dblclick) на тексті задачі активує режим редагування - Текст замінюється на
<input>з поточним значенням Enterабоblur(втрата фокусу) — зберегти зміниEscape— скасувати редагування, повернути старий текст- Порожній текст — видалити задачу
list.addEventListener('dblclick', (e) => {
if (!e.target.classList.contains('todo-text')) return;
const item = e.target.closest('.todo-item');
const id = Number(item.dataset.id);
const todo = todos.find(t => t.id === id);
// Замінити span на input
const input = document.createElement('input');
input.type = 'text';
input.className = 'todo-edit';
input.value = todo.text;
e.target.replaceWith(input);
input.focus();
input.select(); // виділити весь текст
// Зберегти при Enter або blur
function save() {
const newText = input.value.trim();
if (newText) {
todo.text = newText;
} else {
deleteTodo(id);
}
renderTodos();
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') save();
if (e.key === 'Escape') renderTodos(); // скасувати
});
input.addEventListener('blur', save);
});
Частина 5: Фільтрація
Вимоги
Три кнопки-фільтри: Всі, Активні, Виконані.
<div class="todo-filters">
<button class="filter-btn active" data-filter="all">Всі</button>
<button class="filter-btn" data-filter="active">Активні</button>
<button class="filter-btn" data-filter="completed">Виконані</button>
</div>
let currentFilter = 'all';
function getFilteredTodos() {
switch (currentFilter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
}
// Обробник фільтрів
document.querySelector('.todo-filters').addEventListener('click', (e) => {
if (!e.target.classList.contains('filter-btn')) return;
currentFilter = e.target.dataset.filter;
// Оновити активну кнопку
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
renderTodos();
});
Частина 6: Лічильник та очищення
Вимоги
1. Лічильник:
- Показує кількість незавершених задач: "3 завдання залишилось"
- Оновлюється при кожній зміні
function updateCounter() {
const count = todos.filter(t => !t.completed).length;
const counter = document.getElementById('todo-counter');
// Правильне відмінювання: 1 завдання, 2 завдання, 5 завдань
const word = getTaskWord(count);
counter.textContent = `${count} ${word} залишилось`;
}
function getTaskWord(n) {
if (n === 1) return 'завдання';
if (n >= 2 && n <= 4) return 'завдання';
return 'завдань';
}
2. Кнопка "Очистити виконані":
- Видаляє всі задачі зі статусом
completed - Показується тільки якщо є виконані задачі
function clearCompleted() {
todos = todos.filter(t => !t.completed);
renderTodos();
}
Частина 7: localStorage
Вимоги
- Зберігати
todosв localStorage при кожній зміні - Завантажувати з localStorage при відкритті сторінки
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}
function loadTodos() {
const saved = localStorage.getItem('todos');
if (saved) {
todos = JSON.parse(saved);
}
}
// При завантаженні сторінки
loadTodos();
renderTodos();
Частина 8 (бонус): Додаткові можливості
Реалізуй одну або кілька з цих функцій:
1. Drag & Drop (перетягування):
- Зміна порядку задач перетягуванням
- Використай HTML5 Drag and Drop API:
draggable,dragstart,dragover,drop
2. Категорії/теги:
- Можливість додати тег до задачі (наприклад: "Робота", "Дім", "Навчання")
- Фільтрація за тегами
- Кольорові мітки
3. Дата виконання (due date):
- Поле для вибору дати при створенні задачі
- Прострочені задачі підсвічуються червоним
- Сортування за датою
Підказки
Date.now()дає унікальний ID (мілісекунди з 1970 року)- Делегування подій: один слухач на
ul, перевіряємоe.targetвсередині element.replaceWith(newElement)— замінити елемент у DOMinput.select()— виділити весь текст в полі введення- Для drag & drop:
e.preventDefault()вdragoverобов'язковий, інакшеdropне спрацює - Зберігай в localStorage після кожної операції (додавання, видалення, toggle, редагування)
JSON.stringify/JSON.parseдля серіалізації масиву об'єктів
Критерії оцінки
| Критерій | Бали |
|---|---|
| Додавання задач (кнопка + Enter, валідація порожнього рядка) | 15 |
| Позначення виконаних (toggle + візуальний стиль) | 10 |
| Видалення задач | 10 |
| Редагування задач (dblclick, Enter/Escape/blur) | 15 |
| Фільтрація (Всі / Активні / Виконані) | 10 |
| Лічильник залишених задач | 5 |
| Кнопка "Очистити виконані" | 5 |
| Збереження та завантаження з localStorage | 15 |
| Загальна якість коду та стилізації | 15 |
| Бонус: Drag & Drop / Категорії / Due Date | +15 |