Финальный Проект

С возвращением.
Это последний урок курса 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="ru">
<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, спрятанный где-то рядом.