Загрузка данных


Дай пошаговуб инструкцию для выполнения лабораторной работы, я ее делаю в github codespace"# Лабораторная работа: хуки (useState, useEffect)

## Входные файлы

Вам выдаётся приложение «Shelf» — личный трекер чтения:

| Файл             | Назначение                                                             |
| ---------------- | ---------------------------------------------------------------------- |
| `reference.html` | Вёрстка, стили и логика одним файлом — рабочая реализация на чистом JS |

\_Чтобы поднять страницу - можете использовать `Go live` в кодспейсе или `live-server` пакет из npm (`npm install -g live-server` чтобы поставить на всю среду разработки, а не в проект)

## Референс архитектуры приложения

_Обратите внимание, что это архитектура приложения, а не проекта._

_Работаем также в папках `components`, `pages`, в компонентах под каждый компонент создаем папку и кладем туда `jsx` и `css`_

Минимально ожидаемое дерево компонентов:

```
App
 └─ ShelfScreen
     └─ BookForm
          ├─ Checkbox
          ├─ Input
          └─ Button
     ├─ FilterChip
     └─ BookList
          └─ BookItem
```

## Часть 1. Настройка проекта

1. Создайте новое React-приложение (Vite+React+JS+ESLint).

## Часть 2. Разбиение на компоненты и хуки

> Для преобразования `HTML` в `JSX` используйте этот [ сервис ](https://transform.tools/html-to-jsx)

Создайте по одному `.jsx`-файлу на каждый компонент из вашего дерева:

2. **`BookForm`** — контролируемый `input` с собственным `useState` для черновика названия. По кнопке «Добавить на полку» или `Enter` вызывает `onAdd(title)` и очищает своё поле. Пустая/пробельная строка не отправляется.
4. **`BookItem`** — одна книга: цветная обложка-заглушка (можно взять цвет по остатку от деления `id` на длину палитры — как в референсе), название, автор, переключатель «Прочитано» (`onToggleRead`) и кнопка удаления (`onDelete`). Не знает, что такое фильтрация — просто рендерит то, что получил.
5. **`BookList`** — принимает уже отфильтрованный массив `books` и рендерит `BookItem` через `.map()` с `key={book.id}`. Сама фильтрацию не делает.
6. **`ShelfScreen`** — держит логику фильтрации (`books.filter(...)` на основе пропа `showOnlyUnread`) и раздаёт колбэки вниз.
10. **`App`** — единственное место, где находятся все `useState`: `books`, `showOnlyUnread`. Передаёт данные и функции-обработчики вниз по дереву, сам ничего не рендерит напрямую, кроме компоновки дочерних компонентов.

**Важно:** обновления `books` должны быть иммутабельными:

- добавление — через спред (`[...books, newBook]`);
- удаление — через `.filter()`;
- переключение `read` у одной книги — через `.map()`, при этом остальные
  книги должны остаться **теми же объектами** (не пересоздавать их).

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

- Имя компонента и имя файла совпадают и начинаются с заглавной буквы
  (`BookItem.jsx`, а не `bookItem.jsx`).
- Каждый компонент — отдельный файл, экспортируется по умолчанию
  (`export default`).
- Внутри компонентов, принимающих пропсы, используется деструктуризация в
  параметрах функции, а не обращение через `props.что-то` по всему коду.
- Никаких прямых мутаций состояния (`books.push(...)`, `book.read = true`,
  `arr[i] = ...`) — только через `set...` с новым массивом/объектом.
- У каждого элемента списка есть `key`, построенный на основе `id` книги,
  а не на основе индекса массива.
- Значение из `useState` нигде не читается и не пишется напрямую в DOM
  (никаких `document.getElementById(...)` внутри компонентов).

# Как сдавать

- Создайте форк репозитория в вашей организации с названием-этого-репозитория-вашафамилия
- Используя ветку wip сделайте задание
- Зафиксируйте изменения в вашем репозитории
- Захостите работу
- Когда документ будет готов - создайте пул реквест из ветки wip (вашей) на ветку main (тоже вашу) и укажите меня (ktkv419) как reviewer

Не мержите сами коммит, это сделаю я после проверки задания

---

# Памятка

## useState

### Синтаксис

```
const [значение, setЗначение] = useState(начальноеЗначение);
```

- `значение` — то, что читается при рендере.
- `setЗначение` — единственный правильный способ его изменить. Прямое
  присваивание (`значение = ...`) не вызовет повторный рендер и является
  ошибкой.
- Вызов `useState` должен быть на верхнем уровне компонента: не внутри
  `if`, `for` или вложенной функции.

### Базовый пример: переключение состояния (не число, а булево значение)

Похоже на счётчик, но здесь только два состояния — вкл/выкл.

```
const [isOn, setIsOn] = useState(false);

<button onClick={() => setIsOn(prev => !prev)}>
  {isOn ? 'Выключить свет' : 'Включить свет'}
</button>
```

### Функциональное обновление — когда новое значение зависит от старого

Если внутри одного обработчика вызвать `setX(x + 1)` дважды подряд, оба
вызова "увидят" одно и то же старое `x` из замыкания — в сумме получится
`+1`, а не `+2`. Чтобы React брал самое свежее значение, передавайте в
`set...` функцию, а не готовое число:

```
// Плохо: оба вызова используют одно и то же значение score из рендера
setScore(score + 10);
setScore(score + 10);

// Хорошо: каждый вызов получает актуальное значение на момент применения
setScore(prev => prev + 10);
setScore(prev => prev + 10);
```

Это же правило работает и для `setInterval`/`setTimeout` внутри
`useEffect` — там `x` тоже "застревает" на значении из момента запуска
эффекта, если не использовать функциональную форму.

### Обновление массива — без мутации

```
const [playlist, setPlaylist] = useState([]);

// добавить трек
setPlaylist(prev => [...prev, newTrack]);

// удалить трек по id
setPlaylist(prev => prev.filter(track => track.id !== id));

// пометить один трек избранным, остальные не трогать
setPlaylist(prev =>
  prev.map(track => track.id === id ? { ...track, favorite: !track.favorite } : track)
);
```

Правило простое: если структура — массив или объект, `set...` всегда
получает **новый** массив/объект. `push`, `splice`, прямое присваивание
поля (`track.favorite = true`) — так делать нельзя, React не увидит
изменения.

### Обновление объекта-формы

```
const [profile, setProfile] = useState({ name: '', city: '' });

function handleNameChange(e) {
  setProfile(prev => ({ ...prev, name: e.target.value }));
}
```

Забытый спред (`setProfile({ name: e.target.value })` без `...prev`) —
самая частая ошибка: он затирает остальные поля объекта.

### Когда НЕ нужен отдельный useState

Если значение можно посчитать прямо при рендере из уже существующего
state — не заводите под него ещё один `useState`.

```
const [cartItems, setCartItems] = useState([]);

// Не нужно: const [totalCount, setTotalCount] = useState(0);
// Просто считаем при рендере:
const totalCount = cartItems.length;
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
```

---

## useEffect

### Синтаксис

```
useEffect(() => {
  // код эффекта — выполняется после рендера
  return () => {
    // необязательный cleanup — выполняется перед следующим запуском
    // эффекта и перед размонтированием компонента
  };
}, [зависимости]);
```

### Три формы второго аргумента

| Второй аргумент | Когда срабатывает                                                    |
| --------------- | ---------------------------------------------------------------------- |
| не передан      | после **каждого** рендера                                            |
| `[]`            | один раз, после первого рендера (монтирование)                       |
| `[a, b]`        | после первого рендера и затем каждый раз, когда `a` или `b` меняются |

### Пример: без зависимостей — логирование каждого рендера

```
useEffect(() => {
  console.log('Компонент отрендерился');
});
```

Полезно для отладки, но в реальном коде почти всегда нужен второй
аргумент — иначе эффект будет срабатывать без остановки.

### Пример: пустой массив — что-то один раз при монтировании

```
useEffect(() => {
  document.title = 'Открыто новое окно чата';
}, []);
```

### Пример: с зависимостью — реакция на конкретное изменение

```
useEffect(() => {
  console.log(`Пользователь открыл раздел: ${activeTab}`);
}, [activeTab]);
```

Сработает только когда поменяется именно `activeTab`, а не при любом
другом обновлении state в этом же компоненте.

### Пример: debounce

```
useEffect(() => {
  const timer = setTimeout(() => {
    ...
  }, 400);

  return () => clearTimeout(timer);
}, [query]);
```

## Пример: Поиск по строке (фильтрация списка)

Часто нужно отфильтровать список карточек по тому, что ввёл пользователь в поле поиска. Пропсами тут не обойтись одними — понадобится **state**, потому что значение поля ввода меняется во время работы приложения. Общая логика:

1. Состояние строки поиска (`searchQuery`) хранится в **родительском** компоненте (том, у которого есть доступ ко всему массиву данных) — например, в `App`.
2. Поле `<input>` — **контролируемый компонент**: его значение берётся из state, а изменения приходят через `onChange`.
3. Перед рендерингом список фильтруется методом `.filter()` по введённой строке.
4. В дочерний компонент (`CourseList`) передаётся **уже отфильтрованный массив**, а не исходный — сам список ничего не знает о поиске.

```jsx
import { useState } from "react";
import courses from "./data/courses.json";

function App() {
  const [searchQuery, setSearchQuery] = useState("");

  // Фильтруем массив по названию курса, без учёта регистра
  const filteredCourses = courses.filter((course) =>
    course.title.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <>
      <Header />
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <CourseList courses={filteredCourses} />
      <Footer />
    </>
  );
}
```

Компонент поля поиска получает текущее значение и функцию-колбэк через пропсы и вызывает её при каждом вводе — сам он состояние не хранит:

```jsx
function SearchBar({ value, onChange }) {
  return (
    <input
      type="text"
      placeholder="Найти курс..."
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}
```

`CourseList` при этом не меняется вообще — он как получал массив курсов через проп и рендерил его через `.map()`, так и продолжает это делать, не зная, что массив уже отфильтрован:

```jsx
function CourseList({ courses }) {
  if (courses.length === 0) {
    return <p>Курсы не найдены</p>;
  }

  return (
    <div className="courses__grid">
      {courses.map((course) => (
        <CourseCard key={course.id} course={course} />
      ))}
    </div>
  );
}
```

---

## Пример: тудушка (add / update / delete на одном массиве)

Классический учебный пример, который собирает воедино все три операции с массивом в state: добавление через спред, изменение через `.map()`, удаление через `.filter()`. По структуре он очень похож на `Shelf` из задания — задачи вместо книг, `done` вместо `read`.

```jsx
import { useState } from "react";

function App() {
  const [todos, setTodos] = useState([]);
  const [draft, setDraft] = useState("");

  function handleAdd() {
    const title = draft.trim();
    if (title === "") return; // пустую/пробельную строку не добавляем

    const newTodo = {
      id: Date.now(), // простой способ получить уникальный id
      title,
      done: false,
    };

    // ДОБАВЛЕНИЕ — через спред, старые элементы не трогаем
    setTodos((prev) => [...prev, newTodo]);
    setDraft(""); // очищаем поле после добавления
  }

  function handleToggle(id) {
    // ИЗМЕНЕНИЕ — через .map(), пересоздаём только нужный объект,
    // остальные задачи остаются теми же объектами
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }

  function handleDelete(id) {
    // УДАЛЕНИЕ — через .filter(), новый массив без нужного элемента
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }

  return (
    <div>
      <input
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && handleAdd()}
        placeholder="Новая задача..."
      />
      <button onClick={handleAdd}>Добавить</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => handleToggle(todo.id)}
            />
            <span>{todo.title}</span>
            <button onClick={() => handleDelete(todo.id)}>Удалить</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

Три ключевых момента:

- **Добавление** всегда создаёт новый массив: `[...prev, newTodo]`, а не `prev.push(newTodo)`.
- **Изменение одного элемента** — через `.map()`: находим нужный `id`, для него собираем новый объект (`{ ...todo, done: !todo.done }`), для остальных возвращаем тот же самый объект без изменений (`todo`) — так React может пропустить перерисовку неизменившихся элементов.
- **Удаление** — через `.filter()`: возвращаем массив без элемента с нужным `id`, ничего не мутируя.

## Условный рендер через state

Помимо самих данных, в state часто хранят **что показывать**: пустой список, состояние загрузки, открыта ли модалка и т.д. Рендерить в зависимости от такого state можно несколькими способами.

### Тернарный оператор — выбор одного из двух вариантов

```jsx
{
  todos.length === 0 ? (
    <p>Задач пока нет</p>
  ) : (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
```

### `&&` — показать элемент, только если условие истинно

Используется, когда для «иначе» ничего рендерить не нужно.

```jsx
const [showCompleted, setShowCompleted] = useState(false);

<button onClick={() => setShowCompleted((prev) => !prev)}>
  {showCompleted ? "Скрыть выполненные" : "Показать выполненные"}
</button>;

{
  showCompleted && (
    <ul>
      {todos
        .filter((todo) => todo.done)
        .map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
    </ul>
  );
}
```

Осторожно: если слева от `&&` окажется `0` (например, `todos.length && <ul>...`),
React отрендерит на экране `0`, а не «ничего». Поэтому для чисел лучше явно
сравнивать: `todos.length > 0 && ...`.

### Ранний `return` — когда вариантов рендера много

Удобно, когда одно из состояний полностью исключает остальную разметку компонента (например, состояние загрузки).

```jsx
function TodoList({ todos, isLoading }) {
  if (isLoading) {
    return <p>Загрузка...</p>;
  }

  if (todos.length === 0) {
    return <p>Задач пока нет</p>;
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
```

Во всех трёх случаях источник правды один — **state** (`todos`, `showCompleted`, `isLoading`). JSX только читает его и решает, что нарисовать; сам JSX никогда не меняет state напрямую.". а также вот текст из референса, вот файл app.jsx: "import { useState } from 'react'
import heroImg from './assets/hero.png'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>

  <div className="app">
  <div className="app-header">
    <div className="brand">
      <div className="brand-mark">S</div>
      <div className="brand-name">Shelf</div>
    </div>
  </div>
  <section className="screen active" id="screen-shelf">
    <p className="greeting">Добрый вечер</p>
    <div className="add-book-row">
      <input className="input" id="bookInput" placeholder="Название книги..." />
      <button className="btn" id="addBtn">
        Добавить на полку
      </button>
    </div>
    <div className="list-toolbar">
      <span className="toolbar-title">Книги</span>
      <div className="filter-chip">
        <input type="checkbox" id="filterCheckbox" />
        <label htmlFor="filterCheckbox">
          <span className="dot" />
          Только непрочитанные
        </label>
      </div>
    </div>
    <div className="book-list" id="bookList">
      <div className="book-row" data-id={1}>
        <div className="book-cover" style={{ background: "#4f6b52" }}>
          К
        </div>
        <div className="book-info">
          <p className="book-title done">Клара и Солнце</p>
          <div className="book-author">Кадзуо Исигуро</div>
        </div>
        <div className="read-check checked" data-role="toggle">
          <span className="check-circle">✓</span>
          <span className="read-label">Прочитано</span>
        </div>
        <button
          className="delete-btn"
          data-role="delete"
          title="Убрать с полки"
        >
          ✕
        </button>
      </div>
    </div>
  </section>
</div>
    </>
  )
}

export default App
" а это код из файла index.css " :root {
                --ink: #1c1712;
                --surface: #241d17;
                --surface-2: #2c231c;
                --line: #3a3026;
                --paper: #ece4d3;
                --muted: #a89a84;
                --muted-dim: #6f6353;
                --gold: #c9a227;
                --gold-dim: rgba(201, 162, 39, 0.14);
                --wine: #8a3a3a;

                --font-display: 'Lora', Georgia, serif;
                --font-body: 'Inter', system-ui, sans-serif;
                --font-mono: 'IBM Plex Mono', monospace;

                --radius: 12px;
            }

            * {
                box-sizing: border-box;
            }

            body {
                margin: 0;
                background: var(--ink);
                color: var(--paper);
                font-family: var(--font-body);
                font-size: 14px;
                line-height: 1.5;
                -webkit-font-smoothing: antialiased;
                display: flex;
                justify-content: center;
                padding: 48px 20px 80px;
            }

            .app {
                width: 100%;
                max-width: 560px;
            }

            .app-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 24px;
            }

            .brand {
                display: flex;
                align-items: center;
                gap: 10px;
            }

            .brand-mark {
                width: 30px;
                height: 30px;
                border-radius: 8px;
                background: linear-gradient(160deg, var(--gold), #9c7a1a);
                display: flex;
                align-items: center;
                justify-content: center;
                font-family: var(--font-display);
                font-weight: 600;
                color: #1c1712;
                font-size: 15px;
            }

            .brand-name {
                font-family: var(--font-display);
                font-size: 19px;
                font-weight: 500;
            }

            .view-switch {
                display: inline-flex;
                background: var(--surface);
                border: 1px solid var(--line);
                border-radius: 999px;
                padding: 3px;
            }

            .view-btn {
                border: none;
                background: none;
                color: var(--muted);
                font-family: var(--font-body);
                font-size: 12.5px;
                font-weight: 600;
                padding: 7px 14px;
                border-radius: 999px;
                cursor: pointer;
            }

            .view-btn.active {
                background: var(--gold);
                color: #1c1712;
            }

            .screen {
                display: none;
            }
            .screen.active {
                display: block;
            }

            .greeting {
                font-family: var(--font-display);
                font-size: 22px;
                font-style: italic;
                color: var(--paper);
                margin: 0 0 4px;
            }

            .greeting-sub {
                color: var(--muted);
                font-size: 13px;
                margin: 0 0 24px;
            }

            .add-book-row {
                display: flex;
                gap: 8px;
                margin-bottom: 18px;
            }

            .input {
                flex: 1;
                background: var(--surface);
                border: 1px solid var(--line);
                color: var(--paper);
                padding: 11px 14px;
                border-radius: var(--radius);
                font-size: 13.5px;
                font-family: inherit;
            }
            .input::placeholder {
                color: var(--muted-dim);
            }
            .input:focus {
                outline: none;
                border-color: var(--gold);
            }

            .btn {
                background: var(--gold);
                color: #1c1712;
                border: none;
                padding: 11px 18px;
                border-radius: var(--radius);
                font-size: 13.5px;
                font-weight: 600;
                cursor: pointer;
                white-space: nowrap;
            }
            .btn:hover {
                filter: brightness(1.08);
            }

            .btn.ghost {
                background: none;
                border: 1px solid var(--line);
                color: var(--paper);
            }
            .btn.ghost:hover {
                border-color: var(--gold);
                color: var(--gold);
            }

            .list-toolbar {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 12px;
            }

            .toolbar-title {
                font-size: 12px;
                text-transform: uppercase;
                letter-spacing: 0.05em;
                color: var(--muted-dim);
            }

            .filter-chip {
                position: relative;
            }

            .filter-chip input {
                position: absolute;
                opacity: 0;
                width: 0;
                height: 0;
            }

            .filter-chip label {
                display: inline-flex;
                align-items: center;
                gap: 6px;
                font-size: 12.5px;
                color: var(--muted);
                background: var(--surface);
                border: 1px solid var(--line);
                padding: 6px 12px;
                border-radius: 999px;
                cursor: pointer;
                user-select: none;
            }

            .filter-chip input:checked + label {
                background: var(--gold-dim);
                border-color: var(--gold);
                color: var(--gold);
            }

            .filter-chip .dot {
                width: 6px;
                height: 6px;
                border-radius: 50%;
                background: var(--muted-dim);
            }
            .filter-chip input:checked + label .dot {
                background: var(--gold);
            }

            .book-list {
                display: flex;
                flex-direction: column;
                gap: 8px;
            }

            .empty-note {
                color: var(--muted-dim);
                font-size: 13px;
                text-align: center;
                padding: 30px 0;
                border: 1px dashed var(--line);
                border-radius: var(--radius);
            }

            .book-row {
                display: flex;
                align-items: center;
                gap: 13px;
                padding: 11px 12px;
                background: var(--surface);
                border: 1px solid var(--line);
                border-radius: var(--radius);
            }

            .book-cover {
                width: 34px;
                height: 48px;
                border-radius: 4px;
                flex-shrink: 0;
                display: flex;
                align-items: center;
                justify-content: center;
                font-family: var(--font-display);
                font-weight: 600;
                font-size: 15px;
                color: rgba(255, 255, 255, 0.85);
            }

            .book-info {
                flex: 1;
                min-width: 0;
            }

            .book-title {
                font-family: var(--font-display);
                font-size: 14.5px;
                margin: 0;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }

            .book-title.done {
                color: var(--muted);
            }

            .book-author {
                font-size: 12px;
                color: var(--muted-dim);
                margin-top: 1px;
            }

            .read-check {
                display: flex;
                align-items: center;
                gap: 7px;
                cursor: pointer;
                flex-shrink: 0;
            }

            .check-circle {
                width: 20px;
                height: 20px;
                border-radius: 50%;
                border: 1.5px solid var(--muted-dim);
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 11px;
                color: transparent;
            }

            .read-check.checked .check-circle {
                background: var(--gold);
                border-color: var(--gold);
                color: #1c1712;
            }

            .read-label {
                font-size: 11.5px;
                color: var(--muted-dim);
            }

            .delete-btn {
                border: none;
                background: none;
                color: var(--muted-dim);
                font-size: 14px;
                cursor: pointer;
                width: 24px;
                height: 24px;
                border-radius: 6px;
                flex-shrink: 0;
            }
            .delete-btn:hover {
                color: var(--wine);
                background: rgba(138, 58, 58, 0.15);
            }

            .stats-row {
                display: grid;
                grid-template-columns: repeat(3, 1fr);
                gap: 10px;
                margin-bottom: 26px;
            }

            .stat-card {
                background: var(--surface);
                border: 1px solid var(--line);
                border-radius: var(--radius);
                padding: 16px 10px;
                text-align: center;
            }

            .stat-value {
                font-family: var(--font-mono);
                font-size: 24px;
                font-weight: 500;
                color: var(--gold);
            }

            .stat-label {
                font-size: 11.5px;
                color: var(--muted);
                margin-top: 3px;
            }

            .session-card {
                background: var(--surface);
                border: 1px solid var(--line);
                border-radius: var(--radius);
                padding: 22px;
            }

            .session-title {
                font-family: var(--font-display);
                font-size: 16px;
                margin: 0 0 3px;
            }

            .session-sub {
                font-size: 12.5px;
                color: var(--muted);
                margin: 0 0 18px;
            }

            .counter-row {
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 20px;
                margin-bottom: 16px;
            }

            .counter-btn {
                width: 40px;
                height: 40px;
                border-radius: 50%;
                border: 1px solid var(--line);
                background: var(--surface-2);
                color: var(--paper);
                font-size: 18px;
                cursor: pointer;
            }
            .counter-btn:hover {
                border-color: var(--gold);
                color: var(--gold);
            }
            .counter-btn:disabled {
                opacity: 0.35;
                cursor: not-allowed;
            }

            .counter-value {
                font-family: var(--font-mono);
                font-size: 30px;
                min-width: 60px;
                text-align: center;
            }

            .counter-reset {
                display: block;
                margin: 0 auto;
            }"