← 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="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 даёт нам:

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, спрятанный где-то рядом.