← Back to course

Фінальний Проєкт

Фінальний Проєкт

Вітаю знову.

Це остання лекція курсу JavaScript.

Так.

Final boss.

Але не хвилюйся.

Цей boss не дихає вогнем.

Переважно.

У попередніх лекціях ти вивчив багато важливих речей у JavaScript:

Сьогодні ми поєднаємо все це.

Ми створимо маленький task manager.

Не величезний застосунок.

Не startup.

Не імперію продуктивності з мотиваційним dashboard і сімома тарифними планами.

Просто чистий проєкт для початківців.

Застосунок буде:

Це справжній frontend-проєкт.

Маленький.

Корисний.

Дуже JavaScript.

Що Ти Створиш

Ти створиш task manager.

Користувач зможе:

Застосунок також зберігатиме завдання в local storage.

Тому якщо користувач оновить сторінку, завдання залишаться.

Це означає, що ми поєднаємо:

Коротко кажучи, сьогодні JavaScript одягає весь свій гардероб одразу.

Стильно?

Можливо.

Навчально?

Абсолютно.

Створи Проєкт

Створи папку для цієї лекції:

mkdir javascript-lesson12
cd javascript-lesson12
touch index.html
touch script.js
touch tasks.json

Твій проєкт має виглядати так:

javascript-lesson12/
  index.html
  script.js
  tasks.json

Важливо:

Оскільки ми будемо використовувати fetch(), запускай проєкт через локальний сервер.

Наприклад, з Caddy:

caddy file-server --listen :8080

Потім відкрий:

http://localhost:8080

Не відкривай просто index.html подвійним кліком.

Браузер може не дозволити fetch() завантажувати локальні JSON-файли через file://.

У браузерів є правила.

Багато правил.

Деякі корисні.

Деякі виглядають так, ніби їх написав дракон з бюрократичного відділу.

Створи Стартові Дані

Відкрий tasks.json і додай:

[
  {
    "id": 1,
    "title": "Review JavaScript variables",
    "completed": false
  },
  {
    "id": 2,
    "title": "Practice DOM manipulation",
    "completed": false
  },
  {
    "id": 3,
    "title": "Build the final project",
    "completed": false
  }
]

Це масив обʼєктів.

Кожне завдання має:

Це дуже часто зустрічається в реальних проєктах.

Завдання.

Товари.

Користувачі.

Пости блогу.

Замовлення.

Дуже часто дані — це масив обʼєктів.

JavaScript дивиться на масиви обʼєктів і каже:

О так, це дім.

Напиши HTML

Відкрий index.html і додай:

<!DOCTYPE html>
<html lang="uk">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Фінальний Проєкт</title>

  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 60px auto;
      padding: 0 24px;
      background-color: #f3f4f6;
      color: #111827;
    }

    .card {
      background-color: white;
      padding: 24px;
      border: 2px solid #e5e7eb;
      border-radius: 18px;
    }

    h1 {
      font-size: 42px;
      margin-bottom: 8px;
    }

    p {
      font-size: 18px;
      line-height: 1.6;
    }

    form {
      display: flex;
      gap: 10px;
      margin-top: 20px;
    }

    input {
      flex: 1;
      padding: 12px;
      border: 2px solid #d1d5db;
      border-radius: 12px;
      font-size: 18px;
    }

    button {
      background-color: #2563eb;
      color: white;
      border: none;
      padding: 12px 18px;
      border-radius: 999px;
      font-weight: 700;
      cursor: pointer;
    }

    button:hover {
      background-color: #1d4ed8;
    }

    .secondary {
      background-color: #6b7280;
    }

    .secondary:hover {
      background-color: #4b5563;
    }

    .danger {
      background-color: #dc2626;
    }

    .danger:hover {
      background-color: #b91c1c;
    }

    .message {
      margin-top: 18px;
      font-weight: 700;
    }

    .task-list {
      list-style: none;
      padding: 0;
      margin-top: 22px;
      display: grid;
      gap: 12px;
    }

    .task {
      display: flex;
      justify-content: space-between;
      gap: 12px;
      align-items: center;
      padding: 16px;
      border: 2px solid #e5e7eb;
      border-radius: 14px;
      background-color: #f9fafb;
    }

    .task-title {
      font-size: 20px;
      font-weight: 700;
    }

    .completed .task-title {
      text-decoration: line-through;
      color: #6b7280;
    }

    .actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }

    @media (max-width: 600px) {
      form {
        flex-direction: column;
      }

      .task {
        align-items: flex-start;
        flex-direction: column;
      }
    }
  </style>
</head>
<body>
  <h1>Фінальний Проєкт</h1>

  <div class="card">
    <p>
      Додавай завдання, позначай їх як виконані, видаляй їх і оновлюй сторінку.
      Твої завдання залишаться збереженими в браузері.
    </p>

    <form id="taskForm">
      <input id="taskInput" type="text" placeholder="Введи нове завдання">
      <button type="submit">Додати Завдання</button>
    </form>

    <p id="message" class="message">Завантаження завдань...</p>

    <ul id="taskList" class="task-list"></ul>

    <button id="clearButton" class="danger">Очистити Всі Завдання</button>
  </div>

  <script src="script.js"></script>
</body>
</html>

Цей HTML дає нам:

CSS робить це схожим на маленький справжній застосунок.

А не на сіру сторінку з музею стародавнього інтернету.

Добре.

Тепер у нас є стандарти.

Переважно.

Заплануй JavaScript

Перед тим як писати код, зрозуміймо, що має робити застосунок.

Нам потрібно:

Звучить як багато.

Але це просто маленькі частини.

JavaScript-проєкти часто такі.

Одна велика страшна річ перетворюється на багато маленьких нормальних речей.

Як прибирання кімнати.

Страшно, якщо думати про всю кімнату.

Можливо, якщо почати з однієї шкарпетки.

Вибери Елементи

Відкрий script.js і додай:

const taskForm = document.getElementById("taskForm");
const taskInput = document.getElementById("taskInput");
const taskListElement = document.getElementById("taskList");
const messageElement = document.getElementById("message");
const clearButton = document.getElementById("clearButton");

Тепер JavaScript може працювати зі сторінкою.

Ці змінні зʼєднують JavaScript з DOM.

DOM — це міст.

Без цього мосту JavaScript просто говорить сам із собою в кутку.

Іноді це корисно.

Але не сьогодні.

Створи Масив Завдань

Додай це:

let tasks = [];

Цей масив буде зберігати всі завдання, поки застосунок працює.

Кожне завдання буде обʼєктом такого типу:

{
  id: 1,
  title: "Learn JavaScript",
  completed: false
}

Отже, ми маємо:

Масиви й обʼєкти разом.

Класична команда.

Як кава і debugging.

Збережи Завдання в Local Storage

Додай цю функцію:

function saveTasks() {
  localStorage.setItem("tasks", JSON.stringify(tasks));
}

Ця функція зберігає масив tasks у local storage.

Оскільки local storage зберігає рядки, ми використовуємо:

JSON.stringify(tasks)

Це перетворює масив у JSON-рядок.

Без JSON.stringify JavaScript не збереже масив правильно.

Стане дивно.

А JavaScript і так достатньо дивний без допомоги.

Завантаж Завдання з Local Storage

Додай цю функцію:

function loadTasksFromStorage() {
  const savedTasks = localStorage.getItem("tasks");

  if (savedTasks === null) {
    return false;
  }

  tasks = JSON.parse(savedTasks);
  return true;
}

Ця функція перевіряє, чи вже існують завдання в local storage.

Якщо збережених завдань немає, вона повертає false.

Якщо завдання є, вона завантажує їх у масив tasks і повертає true.

Це допомагає нам вирішити:

Використовуй збережені завдання, якщо вони є.
Інакше завантаж стартові завдання з JSON.

Дуже організовано.

Підозріло професійно.

Завантаж Стартові Завдання через Fetch

Додай цю функцію:

async function loadDefaultTasks() {
  try {
    const response = await fetch("tasks.json");

    if (!response.ok) {
      throw new Error("Не вдалося завантажити стартові завдання.");
    }

    tasks = await response.json();
    saveTasks();
  } catch (error) {
    messageElement.textContent = "Не вдалося завантажити стартові завдання.";
    console.log(error);
  }
}

Ця функція завантажує завдання з tasks.json.

Вона використовує:

Це багато попередніх лекцій в одній функції.

Подивись на себе.

Ти використовуєш знання.

Небезпечно.

Але красиво.

Рендер Завдань

Тепер нам потрібно показати завдання на сторінці.

Додай цю функцію:

function renderTasks() {
  taskListElement.innerHTML = "";

  if (tasks.length === 0) {
    messageElement.textContent = "Завдань немає. Додай одне вище.";
    return;
  }

  messageElement.textContent = `У тебе ${tasks.length} завдання/завдань.`;

  for (const task of tasks) {
    const listItem = document.createElement("li");
    listItem.className = task.completed ? "task completed" : "task";

    listItem.innerHTML = `
      <span class="task-title">${task.title}</span>

      <div class="actions">
        <button data-id="${task.id}" class="secondary complete-button">
          ${task.completed ? "Скасувати" : "Готово"}
        </button>

        <button data-id="${task.id}" class="danger delete-button">
          Видалити
        </button>
      </div>
    `;

    taskListElement.appendChild(listItem);
  }
}

Ця функція:

Це DOM rendering.

Дані стають HTML.

HTML стає видимим.

Користувач вражений.

Можливо.

Додай Нове Завдання

Тепер додай цю функцію:

function addTask(event) {
  event.preventDefault();

  const title = taskInput.value.trim();

  if (title === "") {
    messageElement.textContent = "Спочатку введи завдання.";
    return;
  }

  const newTask = {
    id: Date.now(),
    title: title,
    completed: false
  };

  tasks.push(newTask);
  saveTasks();
  renderTasks();

  taskInput.value = "";
}

Ця функція:

Це повний workflow.

Input.

Валідація.

Оновлення даних.

Storage.

Оновлення DOM.

Дуже frontend.

Дуже гарно.

Дуже “здається, я реально починаю розуміти JavaScript.”

Познач Завдання як Виконане

Тепер нам потрібна функція, яка позначає завдання як виконане.

Додай це:

function toggleTask(id) {
  for (const task of tasks) {
    if (task.id === id) {
      task.completed = !task.completed;
    }
  }

  saveTasks();
  renderTasks();
}

Ця функція отримує id.

Потім знаходить відповідне завдання.

Потім перемикає completed.

Якщо було false, стане true.

Якщо було true, стане false.

Ось це робить:

task.completed = !task.completed;

! означає “не”.

Просто.

Потужно.

Маленький символ.

Великий характер.

Видали Завдання

Додай цю функцію:

function deleteTask(id) {
  tasks = tasks.filter(function (task) {
    return task.id !== id;
  });

  saveTasks();
  renderTasks();
}

Тут використовується filter.

Він створює новий масив з усіма завданнями, окрім того, яке ми хочемо видалити.

Цей рядок:

return task.id !== id;

означає:

Залиш кожне завдання, id якого не дорівнює id видаленого завдання.

Завдання з відповідним id зникає.

Дуже чисто.

Без драми.

Завдання йде тихо.

На відміну від деяких багів.

Оброби Кліки по Кнопках Завдань

Кнопки “Готово” і “Видалити” створюються динамічно.

Тому ми будемо слухати кліки на всьому списку завдань.

Додай це:

function handleTaskListClick(event) {
  const id = Number(event.target.dataset.id);

  if (event.target.classList.contains("complete-button")) {
    toggleTask(id);
  }

  if (event.target.classList.contains("delete-button")) {
    deleteTask(id);
  }
}

Це використовує:

event.target

щоб зрозуміти, яку кнопку натиснули.

А це:

event.target.dataset.id

читає data-id з кнопки.

Оскільки значення dataset — це рядки, ми перетворюємо його в число:

Number(event.target.dataset.id)

Це називається event delegation.

Елегантна назва.

Проста ідея.

Слухай на батьківському елементі.

Реагуй на кліки дочірніх елементів.

Як вчитель, який дивиться на весь клас, а не стоїть біля одного учня.

Очисти Всі Завдання

Додай цю функцію:

function clearTasks() {
  tasks = [];
  saveTasks();
  renderTasks();
}

Вона видаляє всі завдання з масиву.

Потім зберігає порожній масив.

Потім оновлює сторінку.

Просто.

Потужно.

Трохи небезпечно.

Саме тому кнопка червона.

Червоні кнопки означають:

Подумай перед тим, як натиснути.

Зазвичай.

Ініціалізуй Застосунок

Тепер потрібно запустити застосунок.

Додай цю функцію:

async function startApp() {
  const hasSavedTasks = loadTasksFromStorage();

  if (!hasSavedTasks) {
    await loadDefaultTasks();
  }

  renderTasks();
}

Вона робить таке:

Це дає застосунку памʼять.

Перший візит?

Завантажує стандартні завдання.

Після цього?

Використовує збережені завдання.

Дуже практично.

Дуже реально.

Додай Event Listeners

Додай це:

taskForm.addEventListener("submit", addTask);
taskListElement.addEventListener("click", handleTaskListClick);
clearButton.addEventListener("click", clearTasks);

startApp();

Тепер застосунок може реагувати на:

І нарешті:

startApp();

запускає все.

Застосунок прокидається.

Сподіваємось, у доброму настрої.

Повний JavaScript Код

Ось повний script.js:

const taskForm = document.getElementById("taskForm");
const taskInput = document.getElementById("taskInput");
const taskListElement = document.getElementById("taskList");
const messageElement = document.getElementById("message");
const clearButton = document.getElementById("clearButton");

let tasks = [];

function saveTasks() {
  localStorage.setItem("tasks", JSON.stringify(tasks));
}

function loadTasksFromStorage() {
  const savedTasks = localStorage.getItem("tasks");

  if (savedTasks === null) {
    return false;
  }

  tasks = JSON.parse(savedTasks);
  return true;
}

async function loadDefaultTasks() {
  try {
    const response = await fetch("tasks.json");

    if (!response.ok) {
      throw new Error("Не вдалося завантажити стартові завдання.");
    }

    tasks = await response.json();
    saveTasks();
  } catch (error) {
    messageElement.textContent = "Не вдалося завантажити стартові завдання.";
    console.log(error);
  }
}

function renderTasks() {
  taskListElement.innerHTML = "";

  if (tasks.length === 0) {
    messageElement.textContent = "Завдань немає. Додай одне вище.";
    return;
  }

  messageElement.textContent = `У тебе ${tasks.length} завдання/завдань.`;

  for (const task of tasks) {
    const listItem = document.createElement("li");
    listItem.className = task.completed ? "task completed" : "task";

    listItem.innerHTML = `
      <span class="task-title">${task.title}</span>

      <div class="actions">
        <button data-id="${task.id}" class="secondary complete-button">
          ${task.completed ? "Скасувати" : "Готово"}
        </button>

        <button data-id="${task.id}" class="danger delete-button">
          Видалити
        </button>
      </div>
    `;

    taskListElement.appendChild(listItem);
  }
}

function addTask(event) {
  event.preventDefault();

  const title = taskInput.value.trim();

  if (title === "") {
    messageElement.textContent = "Спочатку введи завдання.";
    return;
  }

  const newTask = {
    id: Date.now(),
    title: title,
    completed: false
  };

  tasks.push(newTask);
  saveTasks();
  renderTasks();

  taskInput.value = "";
}

function toggleTask(id) {
  for (const task of tasks) {
    if (task.id === id) {
      task.completed = !task.completed;
    }
  }

  saveTasks();
  renderTasks();
}

function deleteTask(id) {
  tasks = tasks.filter(function (task) {
    return task.id !== id;
  });

  saveTasks();
  renderTasks();
}

function handleTaskListClick(event) {
  const id = Number(event.target.dataset.id);

  if (event.target.classList.contains("complete-button")) {
    toggleTask(id);
  }

  if (event.target.classList.contains("delete-button")) {
    deleteTask(id);
  }
}

function clearTasks() {
  tasks = [];
  saveTasks();
  renderTasks();
}

async function startApp() {
  const hasSavedTasks = loadTasksFromStorage();

  if (!hasSavedTasks) {
    await loadDefaultTasks();
  }

  renderTasks();
}

taskForm.addEventListener("submit", addTask);
taskListElement.addEventListener("click", handleTaskListClick);
clearButton.addEventListener("click", clearTasks);

startApp();

Це повний проєкт.

Це вже не іграшковий код.

Все ще дружній для початківців.

Але справжній.

Тепер у тебе є state, rendering, events, storage, validation і fetch.

JavaScript — це вже не тільки console.log.

Він будує речі.

Небезпечно.

У хорошому сенсі.

Протестуй Проєкт

Запусти локальний сервер:

caddy file-server --listen :8080

Відкрий:

http://localhost:8080

Тепер протестуй:

Якщо все працює — вітаю.

Ти створив повний маленький JavaScript-застосунок.

Якщо щось ламається — теж вітаю.

Ти займаєшся справжнім програмуванням.

Справжнє програмування — це здебільшого:

Чому це не працює?
Ага.
Це була одна літера.

Типові Помилки

Забути Локальний Сервер

Якщо завдання не завантажуються, перевір, як ти відкрив сторінку.

Неправильно:

file:///home/user/javascript-lesson12/index.html

Краще:

caddy file-server --listen :8080

Потім:

http://localhost:8080

Fetch хоче сервер.

Навіть маленький.

Браузери суворі.

Як вчителі з сертифікатами безпеки.

Неправильна Назва JSON-Файлу

Якщо код каже:

fetch("tasks.json")

то файл має називатися:

tasks.json

Не:

task.json

Не:

Tasks.json

Не:

final-boss-data.json

Назви файлів мають значення.

Компʼютери не є емоційно гнучкими.

Забути JSON.stringify

Неправильно:

localStorage.setItem("tasks", tasks);

Правильно:

localStorage.setItem("tasks", JSON.stringify(tasks));

Масиви потрібно перетворювати в рядки перед збереженням.

Інакше JavaScript дасть тобі суп.

Не добрий суп.

Забути JSON.parse

Неправильно:

tasks = localStorage.getItem("tasks");

Правильно:

tasks = JSON.parse(savedTasks);

Коли читаєш JSON з local storage, роби parse.

Stringify при збереженні.

Parse при завантаженні.

Запакуй валізу.

Розпакуй валізу.

Не одягай валізу на себе.

Практика

Додай лічильник завдань, який показує:

Виконано: 2 / 5

Підказка:

Можеш порахувати виконані завдання так:

const completedTasks = tasks.filter(function (task) {
  return task.completed;
});

Потім використай:

completedTasks.length

Це дасть тобі практику з:

Маленька функція.

Корисне покращення.

Приємна маленька перемога.

Мінічелендж

Додай пріоритет завдань.

Кожне завдання має мати:

Наприклад:

{
  id: Date.now(),
  title: "Learn fetch",
  priority: "high",
  completed: false
}

Додай <select> в HTML:

<select id="priorityInput">
  <option value="low">Low</option>
  <option value="medium">Medium</option>
  <option value="high">High</option>
</select>

Потім показуй priority у кожній картці завдання.

Бонус:

Використай різні тексти для різних пріоритетів.

Не потрібно робити ідеально.

Спочатку зроби, щоб працювало.

Потім покращуй.

Так ростуть проєкти.

Спочатку картопля.

Потім відполірована картопля.

Потім продукт.

Підсумок

Сьогодні ти створив повний JavaScript-проєкт для початківців.

Ти використав:

Це величезне досягнення.

Ти не просто вивчив синтаксис.

Ти щось створив.

У цьому різниця.

Синтаксис — це словник.

Проєкти — це розмова.

Тепер JavaScript — це вже не тільки теорія.

Це інструмент.

Дивний інструмент.

Іноді драматичний.

Іноді заплутаний.

Але дуже потужний.

Курс Завершено

Ти завершив курс JavaScript для початківців.

Тепер ти знаєш основні ідеї, потрібні для продовження з:

Не поспішай.

Практикуйся.

Будуй маленькі речі.

Ламай їх.

Виправляй.

Потім будуй трохи більші речі.

Це шлях.

Не glamorous.

Не миттєвий.

Але справжній.

І тепер у тебе достатньо JavaScript, щоб почати ним іти.

Вітаю.

Final boss переможено.

Поки що.

JavaScript завжди має ще одного boss, захованого десь неподалік.