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

Вітаю знову.
Це остання лекція курсу JavaScript.
Так.
Final boss.
Але не хвилюйся.
Цей boss не дихає вогнем.
Переважно.
У попередніх лекціях ти вивчив багато важливих речей у JavaScript:
- змінні;
- умови;
- функції;
- масиви;
- обʼєкти;
- DOM;
- події;
- форми;
- валідацію;
- local storage;
- fetch.
Сьогодні ми поєднаємо все це.
Ми створимо маленький task manager.
Не величезний застосунок.
Не startup.
Не імперію продуктивності з мотиваційним dashboard і сімома тарифними планами.
Просто чистий проєкт для початківців.
Застосунок буде:
- завантажувати стартові завдання з JSON-файлу;
- показувати завдання на сторінці;
- дозволяти користувачу додавати нове завдання;
- перевіряти порожній input;
- позначати завдання як виконані;
- видаляти завдання;
- зберігати завдання в local storage;
- памʼятати завдання після оновлення сторінки.
Це справжній frontend-проєкт.
Маленький.
Корисний.
Дуже JavaScript.
Що Ти Створиш
Ти створиш task manager.
Користувач зможе:
- бачити список завдань;
- додавати нові завдання;
- позначати завдання як виконані;
- видаляти завдання;
- очищати всі завдання.
Застосунок також зберігатиме завдання в local storage.
Тому якщо користувач оновить сторінку, завдання залишаться.
Це означає, що ми поєднаємо:
- HTML-структуру;
- CSS-стилі;
- JavaScript-дані;
- рендеринг у DOM;
- event listeners;
- обробку форми;
- валідацію;
- local storage;
- fetch;
- JSON.
Коротко кажучи, сьогодні 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
}
]
Це масив обʼєктів.
Кожне завдання має:
id;title;completed.
Це дуже часто зустрічається в реальних проєктах.
Завдання.
Товари.
Користувачі.
Пости блогу.
Замовлення.
Дуже часто дані — це масив обʼєктів.
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 дає нам:
- форму;
- input;
- список завдань;
- місце для повідомлення;
- кнопку очищення.
CSS робить це схожим на маленький справжній застосунок.
А не на сіру сторінку з музею стародавнього інтернету.
Добре.
Тепер у нас є стандарти.
Переважно.
Заплануй JavaScript
Перед тим як писати код, зрозуміймо, що має робити застосунок.
Нам потрібно:
- вибрати HTML-елементи;
- зберігати завдання в масиві;
- завантажувати завдання з local storage;
- якщо збережених завдань немає, завантажити стартові завдання з
tasks.json; - рендерити завдання на сторінці;
- додавати нове завдання з форми;
- перевіряти порожній input;
- позначати завдання як виконане;
- видаляти завдання;
- очищати всі завдання;
- зберігати зміни в local storage.
Звучить як багато.
Але це просто маленькі частини.
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.
Вона використовує:
fetch;await;response.ok;response.json;try;catch.
Це багато попередніх лекцій в одній функції.
Подивись на себе.
Ти використовуєш знання.
Небезпечно.
Але красиво.
Рендер Завдань
Тепер нам потрібно показати завдання на сторінці.
Додай цю функцію:
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);
}
}
Ця функція:
- очищає список;
- перевіряє, чи є завдання;
- створює
<li>для кожного завдання; - додає кнопки для виконання й видалення;
- вставляє все на сторінку.
Це 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 = "";
}
Ця функція:
- зупиняє перезавантаження форми через
preventDefault; - читає значення input;
- перевіряє порожній input;
- створює новий обʼєкт завдання;
- додає його в масив;
- зберігає завдання;
- знову рендерить завдання;
- очищає input.
Це повний 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();
}
Вона робить таке:
- пробує завантажити завдання з local storage;
- якщо їх немає, завантажує стартові завдання з JSON;
- рендерить завдання.
Це дає застосунку памʼять.
Перший візит?
Завантажує стандартні завдання.
Після цього?
Використовує збережені завдання.
Дуже практично.
Дуже реально.
Додай Event Listeners
Додай це:
taskForm.addEventListener("submit", addTask);
taskListElement.addEventListener("click", handleTaskListClick);
clearButton.addEventListener("click", clearTasks);
startApp();
Тепер застосунок може реагувати на:
- submit форми;
- кліки по кнопках завдань;
- клік по кнопці очищення.
І нарешті:
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
Тепер протестуй:
- стартові завдання завантажуються з
tasks.json; - додавання порожнього завдання показує попередження;
- додавання реального завдання працює;
- клік “Готово” позначає завдання як виконане;
- клік “Скасувати” повертає його назад;
- клік “Видалити” видаляє завдання;
- оновлення сторінки залишає завдання збереженими;
- “Очистити Всі Завдання” видаляє всі завдання.
Якщо все працює — вітаю.
Ти створив повний маленький 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
Це дасть тобі практику з:
- масивами;
filter;- оновленням DOM;
- логікою рендерингу.
Маленька функція.
Корисне покращення.
Приємна маленька перемога.
Мінічелендж
Додай пріоритет завдань.
Кожне завдання має мати:
- title;
- priority.
Наприклад:
{
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-проєкт для початківців.
Ти використав:
- змінні для збереження посилань і даних;
- масиви для збереження завдань;
- обʼєкти для представлення завдань;
- функції для організації логіки;
- DOM-методи для рендерингу завдань;
- події для реакції на користувача;
- форму для отримання input;
- валідацію для блокування порожніх завдань;
- local storage для запамʼятовування завдань;
- fetch для завантаження стартових даних;
- JSON для структурованих даних.
Це величезне досягнення.
Ти не просто вивчив синтаксис.
Ти щось створив.
У цьому різниця.
Синтаксис — це словник.
Проєкти — це розмова.
Тепер JavaScript — це вже не тільки теорія.
Це інструмент.
Дивний інструмент.
Іноді драматичний.
Іноді заплутаний.
Але дуже потужний.
Курс Завершено
Ти завершив курс JavaScript для початківців.
Тепер ти знаєш основні ідеї, потрібні для продовження з:
- TypeScript;
- React;
- Next.js;
- Astro;
- frontend-проєктами;
- сайтами на основі API;
- full-stack застосунками.
Не поспішай.
Практикуйся.
Будуй маленькі речі.
Ламай їх.
Виправляй.
Потім будуй трохи більші речі.
Це шлях.
Не glamorous.
Не миттєвий.
Але справжній.
І тепер у тебе достатньо JavaScript, щоб почати ним іти.
Вітаю.
Final boss переможено.
Поки що.
JavaScript завжди має ще одного boss, захованого десь неподалік.