Загрузка данных
Пошаговая инструкция для лабораторной работы «Shelf»
Часть 1. Настройка проекта в Codespace
Шаг 1. Создание React-приложения через Vite
Откройте терминал в Codespace (Ctrl+~) и выполните:
```bash
npm create vite@latest . -- --template react
```
Если папка не пустая и Vite ругается — выберите «Ignore files and continue» либо создайте проект в подпапке, а затем перенесите содержимое. Далее:
```bash
npm install
```
Шаг 2. Установка зависимостей и запуск
```bash
npm run dev -- --host 0.0.0.0
```
Vite выдаст ссылку — Codespace автоматически предложит открыть порт. Если нет — вкладка Ports → откройте порт 5173 → «Open in Browser».
Шаг 3. Подготовка структуры папок
Внутри src/ создайте:
```
src/
├── components/
│ ├── BookForm/
│ │ ├── BookForm.jsx
│ │ └── BookForm.css
│ ├── BookItem/
│ │ ├── BookItem.jsx
│ │ └── BookItem.css
│ ├── BookList/
│ │ ├── BookList.jsx
│ │ └── BookList.css
│ ├── FilterChip/
│ │ ├── FilterChip.jsx
│ │ └── FilterChip.css
│ ├── Input/
│ │ ├── Input.jsx
│ │ └── Input.css
│ ├── Checkbox/
│ │ ├── Checkbox.jsx
│ │ └── Checkbox.css
│ └── Button/
│ ├── Button.jsx
│ └── Button.css
├── pages/
│ └── ShelfScreen/
│ ├── ShelfScreen.jsx
│ └── ShelfScreen.css
├── App.jsx
├── App.css
├── index.css
└── main.jsx
```
Удалите лишнее из шаблона: assets/hero.png, assets/react.svg, App.css можно оставить пустым или переписать.
Шаг 4. Перенос стилей
Содержимое вашего index.css (которое вы прислали) положите в src/index.css — оно глобальное. Классы типа .book-row, .book-cover, .filter-chip понадобятся внутри компонентов, но можно оставить их в глобальном файле (так проще). Убедитесь, что main.jsx импортирует ./index.css.
---
Часть 2. Реализация компонентов
Пойдём снизу вверх по дереву — от листьев к корню.
Шаг 5. Компонент Button
src/components/Button/Button.jsx:
```jsx
import './Button.css'
export default function Button({ children, onClick, variant = 'primary', type = 'button' }) {
const className = variant === 'ghost' ? 'btn ghost' : 'btn'
return (
<button className={className} type={type} onClick={onClick}>
{children}
</button>
)
}
```
Button.css — можно оставить пустым, стили .btn уже в index.css.
Шаг 6. Компонент Input
src/components/Input/Input.jsx:
```jsx
import './Input.css'
export default function Input({ value, onChange, onKeyDown, placeholder }) {
return (
<input
className="input"
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={placeholder}
/>
)
}
```
Шаг 7. Компонент Checkbox
src/components/Checkbox/Checkbox.jsx:
```jsx
import './Checkbox.css'
export default function Checkbox({ id, checked, onChange, children }) {
return (
<div className="filter-chip">
<input type="checkbox" id={id} checked={checked} onChange={onChange} />
<label htmlFor={id}>
<span className="dot" />
{children}
</label>
</div>
)
}
```
Шаг 8. Компонент BookForm
src/components/BookForm/BookForm.jsx:
```jsx
import { useState } from 'react'
import Input from '../Input/Input'
import Button from '../Button/Button'
import './BookForm.css'
export default function BookForm({ onAdd }) {
const [draft, setDraft] = useState('')
function handleAdd() {
const title = draft.trim()
if (title === '') return
onAdd(title)
setDraft('')
}
function handleKeyDown(e) {
if (e.key === 'Enter') handleAdd()
}
return (
<div className="add-book-row">
<Input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Название книги..."
/>
<Button onClick={handleAdd}>Добавить на полку</Button>
</div>
)
}
```
Ключевые моменты:
· Контролируемый input со своим useState.
· .trim() защищает от пробельных строк.
· onAdd(title) — колбэк из родителя, форма ничего не знает о массиве книг.
Шаг 9. Компонент BookItem
src/components/BookItem/BookItem.jsx:
```jsx
import './BookItem.css'
const COVER_PALETTE = ['#4f6b52', '#8a3a3a', '#5b4a7a', '#3a6b7a', '#7a5b3a']
export default function BookItem({ book, onToggleRead, onDelete }) {
const coverColor = COVER_PALETTE[book.id % COVER_PALETTE.length]
const initial = book.title.charAt(0).toUpperCase()
return (
<div className="book-row">
<div className="book-cover" style={{ background: coverColor }}>
{initial}
</div>
<div className="book-info">
<p className={`book-title ${book.read ? 'done' : ''}`}>{book.title}</p>
<div className="book-author">{book.author}</div>
</div>
<div
className={`read-check ${book.read ? 'checked' : ''}`}
onClick={() => onToggleRead(book.id)}
>
<span className="check-circle">✓</span>
<span className="read-label">Прочитано</span>
</div>
<button
className="delete-btn"
onClick={() => onDelete(book.id)}
title="Убрать с полки"
>
✕
</button>
</div>
)
}
```
Компонент не знает ничего о фильтрации — просто рендерит то, что получил.
Шаг 10. Компонент BookList
src/components/BookList/BookList.jsx:
```jsx
import BookItem from '../BookItem/BookItem'
import './BookList.css'
export default function BookList({ books, onToggleRead, onDelete }) {
if (books.length === 0) {
return <div className="empty-note">На полке пока пусто</div>
}
return (
<div className="book-list">
{books.map((book) => (
<BookItem
key={book.id}
book={book}
onToggleRead={onToggleRead}
onDelete={onDelete}
/>
))}
</div>
)
}
```
BookList тоже не фильтрует — принимает уже отфильтрованный массив.
Шаг 11. Компонент FilterChip
src/components/FilterChip/FilterChip.jsx:
```jsx
import Checkbox from '../Checkbox/Checkbox'
import './FilterChip.css'
export default function FilterChip({ checked, onChange }) {
return (
<Checkbox id="filterCheckbox" checked={checked} onChange={onChange}>
Только непрочитанные
</Checkbox>
)
}
```
Шаг 12. Компонент ShelfScreen
src/pages/ShelfScreen/ShelfScreen.jsx:
```jsx
import BookForm from '../../components/BookForm/BookForm'
import BookList from '../../components/BookList/BookList'
import FilterChip from '../../components/FilterChip/FilterChip'
import './ShelfScreen.css'
export default function ShelfScreen({
books,
showOnlyUnread,
onToggleFilter,
onAdd,
onToggleRead,
onDelete,
}) {
const visibleBooks = showOnlyUnread
? books.filter((book) => !book.read)
: books
return (
<section className="screen active">
<p className="greeting">Добрый вечер</p>
<BookForm onAdd={onAdd} />
<div className="list-toolbar">
<span className="toolbar-title">Книги</span>
<FilterChip checked={showOnlyUnread} onChange={onToggleFilter} />
</div>
<BookList
books={visibleBooks}
onToggleRead={onToggleRead}
onDelete={onDelete}
/>
</section>
)
}
```
Здесь живёт логика фильтрации (books.filter(...)), но не сам массив — он приходит через проп.
Шаг 13. Компонент App — единственное место с useState
src/App.jsx:
```jsx
import { useState } from 'react'
import ShelfScreen from './pages/ShelfScreen/ShelfScreen'
import './App.css'
const INITIAL_BOOKS = [
{ id: 1, title: 'Клара и Солнце', author: 'Кадзуо Исигуро', read: true },
{ id: 2, title: 'Норвежский лес', author: 'Харуки Мураками', read: false },
]
export default function App() {
const [books, setBooks] = useState(INITIAL_BOOKS)
const [showOnlyUnread, setShowOnlyUnread] = useState(false)
function handleAdd(title) {
const newBook = {
id: Date.now(),
title,
author: 'Неизвестный автор',
read: false,
}
setBooks((prev) => [...prev, newBook])
}
function handleToggleRead(id) {
setBooks((prev) =>
prev.map((book) =>
book.id === id ? { ...book, read: !book.read } : book
)
)
}
function handleDelete(id) {
setBooks((prev) => prev.filter((book) => book.id !== id))
}
function handleToggleFilter() {
setShowOnlyUnread((prev) => !prev)
}
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>
<ShelfScreen
books={books}
showOnlyUnread={showOnlyUnread}
onToggleFilter={handleToggleFilter}
onAdd={handleAdd}
onToggleRead={handleToggleRead}
onDelete={handleDelete}
/>
</div>
)
}
```
Разбор иммутабельных обновлений:
Операция Как обновляем Почему так
Добавление [...prev, newBook] новый массив, старые объекты не тронуты
Удаление prev.filter(b => b.id !== id) новый массив без нужного элемента
Переключение read prev.map(b => b.id === id ? {...b, read: !b.read} : b) пересоздаём только нужный объект, остальные — те же самые ссылки
Шаг 14. Уборка
· Уберите всё лишнее из App.css (или очистите файл).
· В main.jsx должно остаться только:
```jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
)
```
· Убедитесь, что <div id="root"> есть в index.html.
Шаг 15. Проверка
1. npm run dev — приложение должно открыться без ошибок в консоли.
2. Добавьте книгу, переключите «Прочитано», удалите, включите фильтр «Только непрочитанные» — всё должно работать.
3. Откройте React DevTools (если есть) — убедитесь, что при переключении read у одной книги остальные объекты не меняются (в Components можно посмотреть ссылки).
4. Проверьте ESLint:
```bash
npm run lint
```
Исправьте предупреждения.
---
Часть 3. Сдача работы
Шаг 16. Форк и ветка
1. Форкните репозиторий в свою организацию, переименуйте в <имя-репо>-<ваша-фамилия>.
2. В Codespace привяжите удалённый репозиторий:
```bash
git remote set-url origin <URL-вашего-форка>
```
3. Создайте ветку wip:
```bash
git checkout -b wip
```
Шаг 17. Коммит и push
```bash
git add .
git commit -m "Лабораторная работа: хуки useState, useEffect — компоненты Shelf"
git push -u origin wip
```
Шаг 18. Хостинг
Самый простой вариант — GitHub Pages через gh-pages:
```bash
npm install --save-dev gh-pages
```
В vite.config.js добавьте:
```js
export default defineConfig({
plugins: [react()],
base: '/<имя-репозитория>/',
})
```
В package.json:
```json
"scripts": {
"deploy": "npm run build && gh-pages -d dist"
}
```
```bash
npm run deploy
```
После этого в настройках репозитория (Settings → Pages) появится URL. Альтернатива — Vercel/Netlify, подключив репозиторий.
Шаг 19. Pull Request
1. На GitHub → Pull requests → New pull request.
2. Base: main (ваш форк), compare: wip.
3. В описании — что сделано, ссылка на задеплоенную версию.
4. В поле Reviewers добавьте ktkv419.
5. Не мержите самостоятельно.
---
Чек-лист по требованиям задания
☐ Имена файлов совпадают с именами компонентов, с большой буквы (BookItem.jsx).
☐ Каждый компонент — отдельный файл, экспорт по умолчанию.
☐ Везде деструктуризация пропсов: function X({ a, b }) {}.
☐ Ни одной мутации: только setBooks/setShowOnlyUnread.
☐ key={book.id}, не индекс.
☐ Нет document.getElementById внутри компонентов.
☐ Все useState — только в App.
☐ BookList не фильтрует, ShelfScreen фильтрует.
☐ Пустая строка в форме не отправляется.
☐ Enter в input добавляет книгу.
☐ useEffect — если не нужен, можно не использовать (по заданию он в теме, но конкретных требований в тексте нет — при желании можно добавить, например, сохранение books в localStorage и чтение при монтировании).
Если хотите добавить useEffect (для полноты темы) — вот пример в App.jsx:
```jsx
import { useState, useEffect } from 'react'
// ...
useEffect(() => {
const saved = localStorage.getItem('shelf-books')
if (saved) setBooks(JSON.parse(saved))
}, [])
useEffect(() => {
localStorage.setItem('shelf-books', JSON.stringify(books))
}, [books])
```
Первый эффект сработает один раз при монтировании, второй — при каждом изменении books. Это бонус, не обязательное требование.