12 Commits
lab5 ... lab6

Author SHA1 Message Date
7f52ebb496 отчет 2025-05-28 19:05:03 +04:00
5408021e1b Отчет 2025-05-26 03:02:49 +04:00
e9d0a1d549 массовое удаление 2025-05-26 00:24:15 +04:00
3293c8b1b9 зум на карточки 2025-05-25 14:28:50 +04:00
e527a79d0c добавил поиск по названию и вернул филтрацию 2025-05-25 13:40:14 +04:00
616e8e76c9 Merge branch 'lab5' into lab6 2025-05-24 20:59:37 +04:00
67fdf675f4 тест 2025-05-24 13:17:21 +04:00
6bbec3f25f fix 2025-05-24 02:49:11 +04:00
d540511a47 робит 2025-05-24 02:47:40 +04:00
7b5012a6e8 fix 2025-05-24 02:39:24 +04:00
78fa452d28 лишнее 2025-05-24 02:27:59 +04:00
f8270722cc готова к сдаче 2025-05-24 02:26:27 +04:00
17 changed files with 479 additions and 5782 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

File diff suppressed because one or more lines are too long

View File

@@ -12,4 +12,4 @@
<div id="root" class="bg-dark"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
</html>

View File

@@ -1,8 +1,13 @@
import React, { useState, useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";
import BookForm from "./components/BookForm";
import BookList from "./components/BookList";
import Home from "./components/Home";
import News from "./components/News";
import Manga from "./components/Manga";
import Author from "./components/Author";
import Reading from "./components/Reading";
import Account from "./components/Account";
import data from "../data.json"; // Используем только для авторов и статусов
function App() {
@@ -10,8 +15,8 @@ function App() {
const [authors] = useState(data.authors);
const [statuses] = useState(data.statuses);
const [editingBook, setEditingBook] = useState(null);
const [statusFilter, setStatusFilter] = useState(""); // пусто — все книги
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("");
// Загрузка книг с сервера
useEffect(() => {
@@ -43,6 +48,20 @@ function App() {
});
};
// Массовое удаление
const deleteSelectedBooks = (ids) => {
Promise.all(
ids.map((id) =>
fetch(`http://localhost:5174/books/${id}`, { method: "DELETE" })
)
).then(() => {
fetch("http://localhost:5174/books")
.then((res) => res.json())
.then((data) => setBooks(data));
});
};
// Обновить книгу
const updateBook = (book) => {
fetch(`http://localhost:5174/books/${book.id}`, {
@@ -56,53 +75,49 @@ function App() {
});
};
// Фильтрация книг по статусу
const filteredBooks = statusFilter
? books.filter(
book =>
statuses.find(s => String(s.id) === String(book.statusId))?.name === statusFilter
)
: books;
// Фильтрация книг по поиску и статусу
const filteredBooks = books.filter(book =>
book.title.toLowerCase().includes(searchQuery.toLowerCase()) &&
(statusFilter === "" ||
statuses.find(s => String(s.id) === String(book.statusId))?.name === statusFilter)
);
return (
<>
<Navbar />
<main className="container py-4">
<h1 className="mb-4">Книги</h1>
{/* --- Фильтр по статусу --- */}
<div className="mb-3" style={{ maxWidth: 350 }}>
<select
className="form-select"
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
>
<option value="">Все статусы</option>
{statuses.map(s => (
<option key={s.id} value={s.name}>{s.name}</option>
))}
</select>
</div>
{/* --- Форма добавления/редактирования --- */}
<BookForm
authors={authors}
statuses={statuses}
onSubmit={editingBook ? updateBook : addBook}
editingBook={editingBook}
onCancel={() => setEditingBook(null)}
/>
{/* --- Список книг (фильтрованные) --- */}
<BookList
books={filteredBooks}
authors={authors}
statuses={statuses}
onEdit={setEditingBook}
onDelete={deleteBook}
/>
<Routes>
<Route
path="/"
element={
<Home
books={filteredBooks}
authors={authors}
statuses={statuses}
editingBook={editingBook}
onAdd={addBook}
onEdit={updateBook}
onDelete={deleteBook}
onCancel={() => setEditingBook(null)}
setEditingBook={setEditingBook}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
onDeleteSelected={deleteSelectedBooks}
/>
}
/>
<Route path="/news" element={<News />} />
<Route path="/manga/:id" element={<Manga />} />
<Route path="/author/:id" element={<Author />} />
<Route path="/reading/:id" element={<Reading />} />
<Route path="/account" element={<Account />} />
</Routes>
</main>
<Footer />
</>
);
}
export default App;
export default App;

View File

@@ -0,0 +1,70 @@
import React, { useState } from "react";
export default function Account() {
const [phone, setPhone] = useState("");
const [password, setPassword] = useState("");
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
setSubmitted(true);
};
return (
<div className="container my-5">
<div className="row justify-content-center">
<div className="col-md-6 bg-secondary p-4 rounded bg-custom-dark">
<form id="loginForm" onSubmit={handleSubmit}>
<h2 className="text-center mb-4">
<i className="bi bi-person-circle me-2"></i>Вход в систему
</h2>
<div className="mb-3">
<label htmlFor="number" className="form-label">
<i className="bi bi-telephone-fill me-2"></i>Номер телефона
</label>
<input
type="tel"
className="form-control"
id="number"
name="number_acc"
placeholder="+7 (___) ___-__-__"
required
value={phone}
onChange={e => setPhone(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="form-label">
<i className="bi bi-lock-fill me-2"></i>Пароль
</label>
<input
type="password"
className="form-control"
id="password"
name="password_acc"
placeholder="Пароль"
required
minLength="4"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
<div className="d-grid">
<button type="submit" className="btn btn-warning">
<i className="bi bi-box-arrow-in-right me-2"></i>Войти
</button>
</div>
{submitted && (
<div className="mt-3 alert alert-info p-2">
<i className="bi bi-info-circle me-2"></i>Проверка формы: пока без логики!
</div>
)}
</form>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import React from "react";
export default function Author() {
return (
<div className="container mt-5">
<div className="row">
<div className="col-md-4 text-center">
<img
className="img-fluid rounded"
src="../img/ХаяоМиядзаки.png" // Используй нужную тебе картинку!
alt="Суи Исида"
/>
</div>
<div className="col-md-8 text-color-light">
<h3>Автор: Фусэ </h3>
<p>
Известен произведением «О моём перерождении в слизь».
В нём автор объединяет элементы западных и восточных ролевых игр и использует идею жанра «исекай» о перерождении или призыве в другой мир.
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,24 +1,64 @@
import React from "react";
import StatusLabel from "./StatusLabel";
import { Link } from "react-router-dom";
function BookCard({ book, authorName, statusName, onEdit, onDelete }) {
function BookCard({
book,
authorName,
statusName,
onEdit,
onDelete,
selected,
onSelect
}) {
return (
<div className="card mb-3 w-100" style={{ maxWidth: "450px", margin: "0 auto" }}>
<img
src={book.cover || "https://placehold.co/200x300"}
className="card-img-top shadow mt-2 mb-3"
alt="Обложка книги"
style={{ height: "300px", objectFit: "contain" }}
<div
className="card card-hover mb-3 w-100"
style={{ maxWidth: "450px", margin: "0 auto", position: "relative" }}
>
{/* Чекбокс для выделения */}
<input
type="checkbox"
checked={selected}
onChange={e => onSelect(book.id, e.target.checked)}
style={{
position: "absolute",
top: 10,
left: 10,
zIndex: 10,
width: "20px",
height: "20px"
}}
title="Выделить для массового удаления"
/>
<Link to={`/manga/${book.id}`}>
<img
src={book.cover || "https://placehold.co/200x300"}
className="card-img-top shadow mt-2 mb-3"
alt="Обложка книги"
style={{ maxHeight: "300px", objectFit: "contain" }}
/>
</Link>
<div className="card-body">
<h5 className="card-title">{book.title}</h5>
<p className="card-text">{book.description}</p>
<p className="card-text"><small>Автор: {authorName}</small></p>
<p className="card-text"><StatusLabel statusName={statusName} /></p>
<button className="btn btn-warning me-2" onClick={() => onEdit(book)}>
<p className="card-text">
<small>Автор: {authorName}</small>
</p>
<p className="card-text">
<StatusLabel statusName={statusName} />
</p>
<button
className="btn btn-warning me-2"
onClick={() => onEdit(book)}
>
Редактировать
</button>
<button className="btn btn-danger" onClick={() => onDelete(book.id)}>
<button
className="btn btn-danger"
onClick={() => onDelete(book.id)}
>
Удалить
</button>
</div>

View File

@@ -1,13 +1,23 @@
import React from "react";
import BookCard from "./BookCard";
function BookList({ books, authors, statuses, onEdit, onDelete }) {
const getAuthorName = (id) => authors.find(a => String(a.id) === String(id))?.name || "Неизвестен";
const getStatusName = (id) => statuses.find(s => String(s.id) === String(id))?.name || "Нет статуса";
function BookList({
books,
authors,
statuses,
onEdit,
onDelete,
selectedIds,
onSelect
}) {
const getAuthorName = (id) =>
authors.find((a) => String(a.id) === String(id))?.name || "Неизвестен";
const getStatusName = (id) =>
statuses.find((s) => String(s.id) === String(id))?.name || "Нет статуса";
return (
<div className="row justify-content-center ">
{books.map(book => (
{books.map((book) => (
<div
className="col-12 col-sm-10 col-md-6 col-lg-4 d-flex align-items-stretch mb-4"
key={book.id}
@@ -18,6 +28,8 @@ function BookList({ books, authors, statuses, onEdit, onDelete }) {
statusName={getStatusName(book.statusId)}
onEdit={onEdit}
onDelete={onDelete}
selected={selectedIds.includes(book.id)}
onSelect={onSelect}
/>
</div>
))}

View File

@@ -0,0 +1,94 @@
import React, { useState } from "react";
import BookForm from "./BookForm";
import BookList from "./BookList";
export default function Home({
books,
authors,
statuses,
editingBook,
onAdd,
onEdit,
onDelete,
onCancel,
setEditingBook,
searchQuery,
setSearchQuery,
statusFilter,
setStatusFilter,
onDeleteSelected // новый пропс!
}) {
// Состояние для выделенных книг
const [selectedIds, setSelectedIds] = useState([]);
// Обработчик выделения карточки
const handleSelect = (id, checked) => {
setSelectedIds(prev =>
checked ? [...prev, id] : prev.filter(selectedId => selectedId !== id)
);
};
// Кнопка массового удаления
const handleDeleteSelected = () => {
if (
selectedIds.length &&
window.confirm(`Удалить ${selectedIds.length} выделенных книг?`)
) {
onDeleteSelected(selectedIds);
setSelectedIds([]); // очистка выделения
}
};
return (
<>
<h1 className="mb-4">Книги</h1>
<div className="mb-3" style={{ maxWidth: 350 }}>
<input
type="text"
className="form-control mb-2"
placeholder="Поиск по названию"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
<select
className="form-select"
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
>
<option value="">Все статусы</option>
{statuses.map(s => (
<option key={s.id} value={s.name}>{s.name}</option>
))}
</select>
</div>
{/* Кнопка массового удаления */}
<div className="mb-2">
<button
className="btn btn-danger"
disabled={selectedIds.length === 0}
onClick={handleDeleteSelected}
>
Удалить выделенные ({selectedIds.length})
</button>
</div>
<BookForm
authors={authors}
statuses={statuses}
onSubmit={editingBook ? onEdit : onAdd}
editingBook={editingBook}
onCancel={onCancel}
/>
<BookList
books={books}
authors={authors}
statuses={statuses}
onEdit={setEditingBook}
onDelete={onDelete}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
</>
);
}

View File

@@ -0,0 +1,37 @@
import React from "react";
import { Link } from "react-router-dom";
export default function Manga() {
return (
<main className="container my-5 flex-grow-1">
<div className="row justify-content-center">
<div className="col-md-8 text-center">
<figure className="figure">
<img
src="/img/заглушка.jpg"
className="figure-img img-fluid rounded"
alt="Токийский Гуль"
style={{ maxHeight: 350, objectFit: "contain" }}
/>
<figcaption className="figure-caption text-light">
О моём перерождении в слизь
</figcaption>
</figure>
<p className="mt-4 text-color-light">
Обычный служащий финансовой компании Сатору Миками погибает, защищая коллегу от грабителя с ножом.
После смерти Сатору попадает в фэнтезийный мир, в котором он предстаёт в виде комка слизи средних размеров по имени Римуру, наделённой немалым разумом.
Отныне Римуру будет жить в мире, полном разных рас, в надежде построить однажды страну, где к каждой расе будут относиться одинаково.
</p>
<div className="d-flex justify-content-center gap-3 mt-3">
<Link to="/author/1" className="btn btn-primary">
<i className="bi bi-person-lines-fill me-2"></i>Про автора
</Link>
<Link to="/reading/1" className="btn btn-success">
<i className="bi bi-book me-2"></i>Читать
</Link>
</div>
</div>
</div>
</main>
);
}

View File

@@ -7,6 +7,7 @@ export default function Navbar() {
<Link className="navbar-brand" to="/">
<img src="/img/manga.png" alt="ЛОГО" height="50" />
</Link>
{/* Бургер-кнопка для мобилок */}
<button
className="navbar-toggler"
type="button"
@@ -20,11 +21,18 @@ export default function Navbar() {
</button>
<div className="collapse navbar-collapse ms-3" id="navbarNavDropdown">
<Link to="/account" className="btn btn-outline-warning">
Вход
</Link>
<ul className="navbar-nav ms-auto mb-2 mb-lg-0">
<li className="nav-item me-2">
<Link to="/news" className="btn btn-outline-warning">
Новости
</Link>
</li>
<li className="nav-item">
<Link to="/account" className="btn btn-outline-warning">
Вход
</Link>
</li>
</ul>
</div>
</nav>
);

View File

@@ -0,0 +1,14 @@
export default function News() {
return (
<div className="container-news mt-4">
<div className="card">
<img src="/img/новость.jpg" className="card-img-top" alt="Новость" />
<div className="card-body">
<h5 className="card-title">Новость</h5>
<p className="card-text">Lorem ipsum dolor sit amet...</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import React from "react";
export default function Reading() {
// Пример массив страниц — можешь заменить на свой массив или получать из API:
const pages = [
"/img/SL1.png",
"/img/SL2.png",
// Можно добавить другие страницы
];
return (
<div className="container mt-5 flex-grow-1">
<div className="row">
<div className="col-12 text-center">
<h3>Читать мангу...</h3>
<div className="d-flex justify-content-center">
{pages.map((src, idx) => (
<img
className="img-reading mx-2"
src={src}
alt={`Манга страница ${idx + 1}`}
key={src}
style={{ maxWidth: 350, maxHeight: 500 }}
/>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,12 @@
.card.card-hover {
transition: transform 0.2s cubic-bezier(.4,2,.3,1);
}
.card.card-hover:hover {
transform: scale(1.04);
z-index: 2;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
}
.bg-custom-dark {
background-color: #0f0630 !important;
@@ -49,4 +58,8 @@ footer {
box-sizing: border-box;
padding: 8px 12px;
font-size: 1rem;
}
.text-color-light {
color: aliceblue;
}

5672
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
{
"name": "int-prog",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "npm-run-all --parallel backend vite",
"vite": "vite",
"build": "vite build",
"serve": "http-server -p 3000 ./html/",
"backend": "json-server ./html/database/data.json -p 5174",
"prod": "npm-run-all build serve --parallel backend serve",
"lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap-icons": "^1.11.3",
"inputmask": "^5.0.9"
},
"devDependencies": {
"eslint": "8.56.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.2",
"eslint-plugin-html": "8.1.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "5.2.3",
"http-server": "14.1.1",
"json-server": "^1.0.0-beta.3",
"npm-run-all": "4.1.5",
"vite": "6.2.0"
}
}

BIN
Отчет №6.docx Normal file

Binary file not shown.