Вивчай
Домашнє завдання #14 ·
балівintermediate

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">&times;</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) — замінити елемент у DOM
  • input.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
Збереження та завантаження з localStorage15
Загальна якість коду та стилізації15
Бонус: Drag & Drop / Категорії / Due Date+15