Перейти к содержанию

Лабораторная работа №4. Реализация клиентской бизнес-логики Web-приложения

1. Теория

1.0. Цель работы

Сформировать у обучающегося системное понимание JavaScript как языка клиентской логики Web-приложения, который:

  • реализует бизнес-логику и сценарии пользователя на клиенте;
  • управляет интерфейсом через DOM и события;
  • взаимодействует с Web-сервисом через HTTP (REST API);
  • обрабатывает данные (массивы/объекты), выполняет валидацию, фильтрацию, сортировку, агрегацию;
  • работает в условиях асинхронности (Promise, fetch, обработка ошибок);
  • строит минимальную архитектуру: UI слой → логика → API слой.

Закрепить навык архитектурного описания клиентской части через схему:

UI (HTML) → DOM → JS (events/state) → HTTP (fetch) → REST API → DB

Подводка (почему это важно): Во взрослом Web-проекте JavaScript — это не “скрипты для кнопок”. Это мозг клиентской части, который связывает:

  • данные сервера (JSON) ↔ визуальные компоненты UI,
  • действия пользователя ↔ бизнес-правила (валидация, права, статусы),
  • состояния интерфейса ↔ сетевые операции (loading/error/success),
  • структуру HTML ↔ стили CSS ↔ логику JS (через классы и data-*).

Если JS построен правильно, то:

  • интерфейс легко расширяется (добавили фильтр/поиск — не переписываем всё);
  • ошибки сети и валидации обрабатываются предсказуемо;
  • код разделён на слои и сопровождаем командой.

1.1. Введение в JavaScript как язык клиентской логики

1.1.1. Роль JavaScript в архитектуре Web

В Web-приложении есть три базовых слоя:

  • HTML — структура (UI-скелет) семантика, формы, контейнеры, точки привязки для JS.
  • CSS — представление (визуальный слой) layout, адаптивность, состояния (через классы), UX.
  • JavaScript — поведение (логика) события, валидация, отрисовка данных, работа с API, состояния.

Подводка: современный интерфейс “живёт” за счёт данных. Значит JS должен уметь: получить данные → преобразовать → показать → обработать действия → отправить обратно.


1.1.2. Связь JS и REST API

REST API — это серверные ресурсы, доступные по HTTP. На клиенте JS обычно реализует:

  • загрузку списков (GET),
  • создание записи (POST),
  • обновление (PUT/PATCH),
  • удаление (DELETE).

Мини-схема CRUD:

User action → JS → fetch() → HTTP → API → DB
                    ↓
                 JSON response
                    ↓
             JS updates DOM → UI

Мини-пример (чтение данных):

fetch("/api/users")
  .then((res) => res.json())
  .then((users) => console.log(users))
  .catch((err) => console.error("Network error:", err));

1.2. Где выполняется JavaScript

1.2.1. Браузер и среда выполнения

В браузере JS выполняется в движке (например, V8/SpiderMonkey). У JS есть доступ к:

  • DOM API (элементы страницы),
  • Web API (таймеры, fetch, storage и т.д.),
  • консоли разработчика.

Подводка: важно различать:

  • JS язык (синтаксис, типы, функции),
  • Web API (то, что даёт браузер: DOM, fetch, localStorage).

1.2.2. Консоль разработчика

Консоль — инструмент для:

  • проверки выражений,
  • логирования (console.log, console.table),
  • диагностики ошибок,
  • просмотра сетевых запросов (вкладка Network).

1.2.3. Подключение JS к HTML

Рекомендуемая практика для UI: defer.

<script src="./app.js" defer></script>

Почему defer важен:

  • HTML строит DOM,
  • затем запускается JS,
  • значит querySelector не вернёт null из-за “раннего запуска”.

1.3. Базовый синтаксис JavaScript

1.3.1. Типы данных

Тип Пример Где встречается в UI
string "Anna" ввод в форму, текст кнопок
number 1200 цена, количество, рейтинг
boolean true/false чекбоксы, флаги состояния
null null “нет значения” (осознанно)
undefined undefined “не задано” (ошибка/пропуск)
object {...} записи, ответы API, настройки

Подводка: JSON из API — это почти всегда “объекты + массивы”, поэтому базовое мышление JS-разработчика — работа с Array и Object.


1.3.2. Переменные: let, const, область видимости

  • const — значение ссылки неизменно (но объект можно менять внутри)
  • let — значение можно переназначать
const user = { id: 1, name: "Anna" };
user.name = "Ann"; // допустимо

let counter = 0;
counter += 1; // допустимо

Область видимости:

  • let/const — блочная (внутри {}),
  • var в современном коде избегают.

1.3.3. Операторы

  • арифметические: + - * / %
  • сравнения: === !== > < >= <=
  • логические: && || !

Подводка: для условий в UI критично использовать ===, чтобы избежать скрытых преобразований типов.


1.4. Управляющие конструкции

1.4.1. Условия и циклы

if (value > 0) { /* ... */ }
else { /* ... */ }

switch (status) {
  case "new": break;
  case "done": break;
  default: break;
}

for (let i = 0; i < arr.length; i++) {}
while (cond) {}

forEach для массивов:

items.forEach((item) => console.log(item.title));

1.4.2. Примеры “реальных задач”

Проверка данных формы:

function isEmailValid(email) {
  return email.includes("@") && email.includes(".");
}

if (!isEmailValid(email)) {
  showError("Некорректный email");
}

Фильтрация массива:

const newItems = items.filter((x) => x.status === "new");

1.5. Функции и модульность

1.5.1. Объявление функций

function sum(a, b) { return a + b; }

const sum2 = (a, b) => a + b;

1.5.2. Параметры и возврат значения

Подводка: клиентская логика должна быть “проверяемой”. Поэтому функции должны возвращать результат, а не печатать всё в DOM напрямую.


1.5.3. Чистые функции

Чистая функция не изменяет внешние переменные и при одинаковом вводе даёт одинаковый вывод.

function calcTotal(items) {
  return items.reduce((acc, x) => acc + x.value, 0);
}

Подводка: чистые функции легче тестировать и переносить между проектами.


1.5.4. Разделение логики по слоям

Мини-архитектура:

  • api.js — запросы к серверу
  • logic.js — обработка данных (фильтры, сортировка, валидация)
  • ui.js/script.js — DOM и события

Схема:

UI (DOM/events) → Logic (pure functions) → API (fetch) → Server

1.6. Работа с массивами и объектами

1.6.1. Массивы: map, filter, find, reduce

const titles = items.map((x) => x.title);
const onlyNew = items.filter((x) => x.status === "new");
const found = items.find((x) => x.id === 10) || null;

const stats = items.reduce((acc, x) => {
  acc.total += 1;
  acc.sum += x.value;
  return acc;
}, { total: 0, sum: 0 });

1.6.2. Объекты и вложенные структуры

const user = { id: 1, profile: { name: "Anna", role: "admin" } };
console.log(user.profile.role);

1.6.3. JSON как формат объекта

const users = [
  { id: 1, name: "Anna", role: "admin" }
];

const json = JSON.stringify(users);
const parsed = JSON.parse(json);

Подводка: REST API почти всегда общается JSON-ом, поэтому JSON.parse/stringify — фундамент.


1.7. DOM и управление интерфейсом

1.7.1. Что такое DOM

DOM — дерево объектов, построенное из HTML. JS управляет UI через DOM-узлы: создаёт, удаляет, изменяет.


1.7.2. Поиск элементов

const title = document.querySelector(".title");
const form = document.getElementById("loginForm");

1.7.3. Изменение содержимого

el.textContent = "Загрузка...";
el.innerHTML = "<strong>OK</strong>"; // осторожно
el.classList.add("is-active");

Подводка: innerHTML опаснее (риск XSS), предпочтительнее создавать элементы через createElement.


1.7.4. События

btn.addEventListener("click", () => {});
form.addEventListener("submit", (e) => { e.preventDefault(); });
input.addEventListener("input", () => {});

Реальный пример (добавление записи в таблицу):

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const title = form.elements.title.value;

  const row = document.createElement("div");
  row.textContent = title;
  list.appendChild(row);
});

1.8. Работа с формами (ключевой блок)

1.8.1. Получение значений полей

const email = form.elements.email.value.trim();
const password = form.elements.password.value;

1.8.2. Валидация и ошибки UI

function validateLogin(email, password) {
  const errors = [];
  if (!email.includes("@")) errors.push("Email некорректен");
  if (password.length < 8) errors.push("Пароль минимум 8 символов");
  return errors;
}

1.8.3. Отмена стандартного поведения

form.addEventListener("submit", (e) => {
  e.preventDefault();
});

1.8.4. Отображение ошибок пользователю

const errorBox = document.querySelector("#errors");
errorBox.textContent = errors.join("\n");

1.9. Асинхронность и взаимодействие с API

1.9.1. Что такое асинхронность

Сеть и таймеры работают не мгновенно. JS использует:

  • Promises,
  • async/await,
  • обработку ошибок.

1.9.2. Promise и fetch

fetch("/api/users")
  .then((res) => res.json())
  .then((data) => renderUsers(data))
  .catch((err) => showError("Ошибка сети"));

1.9.3. async/await (предпочтительно для читаемости)

async function loadUsers() {
  try {
    const res = await fetch("/api/users");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const users = await res.json();
    renderUsers(users);
  } catch (err) {
    showError(err.message);
  }
}

1.9.4. Обработка ошибок и статусов

  • 200–299 — успех
  • 400 — ошибка клиента (валидация)
  • 401/403 — доступ
  • 404 — не найдено
  • 500 — серверная ошибка

Подводка: UI должен уметь показывать loading/error/success.


1.10. Реализация клиентской бизнес-логики

1.10.1. CRUD на клиенте

  • Create: отправили форму → POST → добавили в список
  • Read: GET → отрисовали список
  • Update: PATCH/PUT → обновили карточку
  • Delete: DELETE → убрали карточку

1.10.2. Управление состоянием интерфейса

Типовые состояния:

  • is-loading — идёт запрос
  • is-error — ошибка
  • is-empty — нет данных
  • is-open — панель открыта

Подход: состояние хранится в классах + переменных состояния.


1.10.3. Мини-архитектура (слои)

UI Layer: DOM + events + render()
Logic Layer: validate/filter/sort/stats (pure functions)
API Layer: fetch wrappers (get/post/patch/delete)

1.11. Обработка ошибок и пользовательский опыт

1.11.1. try/catch и сообщения

try {
  // fetch...
} catch (err) {
  message.textContent = "Не удалось выполнить операцию";
}

1.11.2. Блокировка кнопки во время запроса

btn.disabled = true;
btn.textContent = "Отправка...";

try {
  // await fetch
} finally {
  btn.disabled = false;
  btn.textContent = "Отправить";
}

Подводка: UX — это не “красиво”, а “понятно и предсказуемо при ошибках”.


1.12. Защита и безопасность (базовый уровень)

1.12.1. Валидация данных

Клиентская валидация нужна для UX, но сервер всегда перепроверяет.


1.12.2. XSS (понятие)

XSS возникает, когда вы вставляете непроверенные строки как HTML:

// опасно, если userInput содержит HTML/скрипты
container.innerHTML = userInput;

Безопаснее:

container.textContent = userInput;

1.12.3. Почему нельзя доверять клиенту

Пользователь может:

  • отключить JS,
  • подменить запрос,
  • отправить любые данные через Postman/DevTools.

Вывод: клиент — “удобство”, сервер — “истина”.


2. Задание

Цель: закрепить базовый синтаксис JavaScript, работу с типами и переменными, условиями и циклами, разработку функций, обработку массивов/объектов, работу с DOM и событиями, а также основы асинхронности и взаимодействия с API на материале клиентской логики Web-приложения.


Смысл лабораторной работы

Переход от “JS в консоли” к “JS как клиентская логика приложения”:

  • данные представлены в виде массивов объектов и обрабатываются функциями;
  • интерфейс строится и обновляется через DOM;
  • действия пользователя обрабатываются через events;
  • сетевые запросы выполняются асинхронно, а UI показывает состояния loading/error/success;
  • ошибки типов и преобразований контролируются явно.

Ключевой результат

Интерактивная страница (или набор скриптов), в которой:

  • созданы переменные разных типов, выведены типы через typeof;
  • продемонстрированы “опасные” преобразования типов и исправления через явное приведение;
  • реализованы проверки данных формы и обработка массивов (фильтрация/подсчёт статистики);
  • написаны функции validateUser(formData) и buildStats(items) (через reduce);
  • сформирован список карточек через createElement;
  • обработан click делегированием через data-action;
  • реализована загрузка данных loadItems() через async/await с проверкой res.ok и выводом ошибок пользователю.

2.1. Задания на закрепление теоретического материала (выполнить до мини-проекта)

Данный блок выполняется в отдельном файле practice.js (или в конце script.js), чтобы студент мог продемонстрировать понимание базовых инструментов JS до интеграции с предметной областью.

Требования к оформлению:

  • каждая задача должна завершаться console.log(...) (или console.table(...)) с демонстрацией результата;
  • все примеры должны быть воспроизводимы при открытии страницы;
  • запрещено использовать сторонние библиотеки;
  • для задач с DOM должен быть минимальный HTML-фрагмент в index.html.

2.1.1. Типы и переменные

Задача 1.1. Создать 10 переменных разных типов и значений.

Требования:

  • минимум по одному: string, number, boolean, null, undefined, object, array, function, date, bigint (или symbol).

Пример:

const fullName = "Anna";                 // string
const price = 1999;                      // number
const isActive = true;                   // boolean
const emptyValue = null;                 // null
let notDefined;                          // undefined
const user = { id: 1, role: "admin" };   // object
const values = [10, 20, 30];             // array (object)
const sum = (a, b) => a + b;             // function
const createdAt = new Date();            // object (Date)
const big = 9007199254740993n;           // bigint

Задача 1.2. Вывести типы переменных через typeof.

Требования:

  • вывести пары “значение → тип” в консоль;
  • показать “подводные камни” типов: массив и null.

Пример:

console.log(typeof fullName);    // "string"
console.log(typeof values);      // "object" (важный момент)
console.log(typeof emptyValue);  // "object" (историческая особенность)

Задача 1.3. Сделать 3 примера “опасных” преобразований и исправить через явное приведение.

Требования:

  • для каждого примера показать: “как было” → “почему опасно” → “как исправили”.

Примеры (типовые):

  1. Строка + число:
console.log("10" + 5); // "105" (склейка строк)

console.log(Number("10") + 5); // 15 (исправление)
  1. Сравнение с ==:
console.log("0" == 0);  // true (неявное приведение)
console.log("0" === 0); // false (строгое сравнение)

console.log(Number("0") === 0); // true (явное приведение + строгая проверка)
  1. Преобразование к boolean:
console.log(Boolean("false")); // true (любая непустая строка true)

const raw = "false";
const parsed = raw === "true";
console.log(parsed); // false (исправление под бизнес-логику)

Ожидаемый результат:

  • в консоли видны все 3 кейса и корректные исправления.

2.1.2. Условия и циклы

Задача 2.1. Проверить поля формы на пустоту.

HTML (пример):

<form id="userForm">
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">OK</button>
</form>
<p id="formState"></p>

JS (требование):

  • в обработчике submit запретить отправку (preventDefault);
  • если email или password пустые → показать сообщение об ошибке.

Пример:

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const email = form.elements.email.value.trim();
  const password = form.elements.password.value;

  if (!email || !password) {
    state.textContent = "Ошибка: заполните email и пароль";
    return;
  }

  state.textContent = "ОК";
});

Задача 2.2. Вывести элементы массива по условию.

Требования:

  • создать массив чисел;
  • вывести только те, что удовлетворяют условию (например, >= 1000).

Пример:

const prices = [500, 1200, 800, 1500, 300];
for (const p of prices) {
  if (p >= 1000) console.log(">=1000:", p);
}

Задача 2.3. Построить подсчёт статистики по массиву объектов.

Требования:

  • массив items (минимум 5 объектов): { id, title, value, status };
  • посчитать: количество, сумму value, количество status="new".

Пример (разрешены циклы или reduce):

const items = [
  { id: 1, title: "A", value: 1200, status: "new" },
  { id: 2, title: "B", value: 800, status: "done" },
];

let totalCount = 0;
let sumValue = 0;
let newCount = 0;

for (const x of items) {
  totalCount += 1;
  sumValue += x.value;
  if (x.status === "new") newCount += 1;
}

console.log({ totalCount, sumValue, newCount });

2.1.3. Функции

Задача 3.1. Реализовать validateUser(formData), возвращающую массив ошибок.

Требования:

  • вход: объект { email, password };
  • выход: массив строк ошибок (если ошибок нет — пустой массив);
  • проверка: email содержит @, пароль длиной минимум 8.

Пример:

function validateUser(formData) {
  const errors = [];
  const email = String(formData.email ?? "").trim();
  const password = String(formData.password ?? "");

  if (!email.includes("@")) errors.push("Email некорректен");
  if (password.length < 8) errors.push("Пароль минимум 8 символов");

  return errors;
}

Задача 3.2. Реализовать buildStats(items) через reduce.

Требования:

  • вход: массив объектов { value, status };
  • выход: объект { totalCount, sumValue, maxValue, newCount }.

Пример:

function buildStats(items) {
  return items.reduce((acc, x) => {
    acc.totalCount += 1;
    acc.sumValue += x.value;
    if (x.value > acc.maxValue) acc.maxValue = x.value;
    if (x.status === "new") acc.newCount += 1;
    return acc;
  }, { totalCount: 0, sumValue: 0, maxValue: 0, newCount: 0 });
}

Ожидаемый результат:

  • функции вызываются и выводят результат в консоль.

2.1.4. DOM

Задача 4.1. Создать список карточек через createElement.

HTML (пример):

<div id="cards"></div>

Требования:

  • отрисовать массив объектов как карточки;
  • карточка показывает минимум: title, value, status.

Пример (принцип):

function renderCards(itemsToRender) {
  cards.textContent = "";
  for (const item of itemsToRender) {
    const card = document.createElement("article");
    card.className = "card";

    const h3 = document.createElement("h3");
    h3.textContent = item.title;

    const p = document.createElement("p");
    p.textContent = `value=${item.value} | status=${item.status}`;

    card.append(h3, p);
    cards.appendChild(card);
  }
}

Задача 4.2. Обработать событие click делегированием через data-action.

Требования:

  • в каждой карточке добавить кнопку с data-action="remove" и data-id;
  • обработчик клика навешивается на контейнер #cards;
  • при клике на кнопку выводить id удаляемого элемента (или удалять из массива).

Пример:

cards.addEventListener("click", (e) => {
  const btn = e.target.closest("button[data-action]");
  if (!btn) return;

  const action = btn.dataset.action;
  const id = Number(btn.dataset.id);

  if (action === "remove") {
    console.log("remove id:", id);
  }
});

2.1.5. Асинхронность

Задача 5.1. Реализовать loadItems() через async/await.

Требования:

  • выполнить fetch("/api/items") (или любой учебный endpoint);
  • распарсить JSON;
  • передать данные в renderCards.

Пример:

async function loadItems() {
  const state = document.querySelector("#apiState");
  state.textContent = "Загрузка...";

  try {
    const res = await fetch("/api/items");

    if (!res.ok) {
      state.textContent = `Ошибка загрузки: HTTP ${res.status}`;
      return;
    }

    const data = await res.json();
    state.textContent = `Загружено: ${data.length}`;
    renderCards(data);
  } catch (err) {
    state.textContent = "Ошибка сети. Повторите позже.";
  }
}

Задача 5.2. Обработать res.ok и вывести статус ошибки пользователю.

Требования:

  • при !res.ok показать статус (400/401/404/500);
  • не пытаться res.json() без необходимости (если сервер не гарантирует JSON при ошибке).

Ожидаемый результат:

  • UI показывает корректное сообщение об ошибке, а не “сломанный” скрипт.

2.2. Исходные данные (учебные)

Для выполнения практик используются:

  1. локальный массив объектов items (минимум 5–8 объектов) для задач 2–4;
  2. форма #userForm для задач 2.1 и 3.1;
  3. endpoint /api/items (учебный/заглушка) для практики 5 — допускается заменить на публичный тестовый API или мок-данные.

2.3. Структура проекта (рекомендуется)

Минимальный набор файлов:

  • index.html — форма, контейнер #cards, блок #apiState;
  • script.js — весь код практик;
  • (опционально) styles.css — минимальная читабельная стилизация карточек.

Подключение:

<script src="./script.js" defer></script>

2.4. Обязательные требования к сдаче

Студент обязан продемонстрировать:

  1. 10 переменных разных типов + вывод typeof;
  2. 3 “опасных” преобразования + исправления через явное приведение;
  3. проверку формы на пустоту (submit + preventDefault);
  4. фильтрацию/вывод по условию и статистику по массиву объектов;
  5. validateUser(formData) → массив ошибок;
  6. buildStats(items) через reduce;
  7. рендер карточек через createElement;
  8. делегирование клика через data-action;
  9. loadItems() через async/await + проверка res.ok + вывод ошибки пользователю.

2.5. Для выполнения требуется

  1. Шаблон отчёта, указанный в описании лабораторной работы.
  2. Вариант задания, закреплённый за студентом на семестр и опубликованный в ЛМС.
  3. Требования к оформлению — согласно методическим указаниям в ЛМС или соответствующему разделу документации.
  4. Ссылка на репозиторий (или архив проекта).

2.6. Запрет

Запрещается использование систем искусственного интеллекта для выполнения лабораторной работы.

Работы, выполненные с использованием ИИ, полностью или частично скопированные, а также не сданные, оцениваются в 0 баллов без возможности доработки.


3. Контрольные вопросы

  1. Что такое JavaScript и какую роль он играет в архитектуре HTML (UI) → DOM → JS (events/state) → HTTP → API → DB?
  2. Чем отличаются тип данных и значение в JS? Какие типы являются примитивами?
  3. Что возвращает оператор typeof и какие “подводные камни” у typeof null и typeof []?
  4. В чём разница между let и const? Что означает “нельзя переназначить ссылку”, но “можно менять объект”?
  5. Приведите 3 примера неявного преобразования типов (coercion) и объясните, почему оно опасно в бизнес-логике.
  6. Почему в условиях и проверках предпочтительнее ===, а не ==? Приведите пример, где == даёт неожиданное поведение.
  7. Как корректно проверить “пустоту” строки ввода формы (с учётом пробелов) и почему нужен trim()?
  8. Чем отличается for, for...of и forEach? Когда стоит избегать forEach?

5. Чек-лист для самопроверки

Баллы Критерии оценки
20 Выполнены все практики 1–5 и они явно демонстрируются (в консоли и/или на странице). Реализовано: Практика 1 — 10 переменных разных типов + вывод typeof + 3 “опасных” преобразования и исправления через явное приведение (Number, String, Boolean, строгие сравнения ===). Практика 2 — проверка формы на пустоту (submit, preventDefault, trim) + вывод элементов массива по условию (цикл/условие) + подсчёт статистики по массиву объектов (count/sum/newCount). Практика 3 — функция validateUser(formData) возвращает массив ошибок (минимум: email/пароль) + buildStats(items) реализована через reduce и возвращает объект статистики. Практика 4 — список карточек создаётся через createElement (без вывода “простынёй” через innerHTML) + обработка кликов выполнена делегированием через контейнер и data-action/data-id. Практика 5 — реализована loadItems() через async/await, корректно проверяется res.ok, ошибки (HTTP status и сеть) выводятся пользователю в UI (например, #apiState), приложение не падает. Код аккуратно оформлен, без дублирования логики, в консоли нет ошибок.
16–19 Почти всё выполнено: практики 1–5 реализованы, но есть 1–2 недочёта. Например: один тип/пример в практике 1 отсутствует; “опасные преобразования” показаны, но исправление не полностью корректно; статистика считается, но без одного поля (например, maxValue); DOM-рендер есть, но местами используется innerHTML без необходимости; делегирование событий реализовано, но не через closest() и ломается при клике по вложенным элементам; loadItems() работает, но сообщения об ошибках пользователю недостаточно информативны.
12–15 Выполнено частично: выполнены 3–4 практики из 5. Часто: типы/переменные и условия сделаны, но нет reduce-статистики или нет validateUser; DOM-рендер есть, но без делегирования; loadItems() отсутствует или без проверки res.ok; ошибки сети не обработаны (есть падения/Unhandled Promise).
8–11 Есть отдельные фрагменты JS: переменные/условия/циклы показаны, но практики выполнены формально. Нет системной валидации функцией, нет статистики через reduce, DOM формируется не через createElement (или выводится “текстом”), событийная модель не продумана, асинхронность отсутствует или реализована некорректно.
1–7 Формальный шаблон: есть подключение script.js, но задачи не выполнены или не демонстрируются. Типы не показаны, преобразования не разобраны, форма не проверяется, DOM не обновляется, клики не обрабатываются, loadItems() отсутствует.
0 Работа не сдана / неработоспособна (ошибки в консоли, код не запускается) / результат не соответствует требованиям выполнения практик.

5. Шаблон отчёта

👉 Скачать шаблон отчёта