массовое удаление
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -48,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}`, {
|
||||
@@ -90,6 +104,7 @@ function App() {
|
||||
setSearchQuery={setSearchQuery}
|
||||
statusFilter={statusFilter}
|
||||
setStatusFilter={setStatusFilter}
|
||||
onDeleteSelected={deleteSelectedBooks}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,26 +2,63 @@ 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 card-hover mb-3 w-100" style={{ maxWidth: "450px", margin: "0 auto" }}>
|
||||
<Link to={`/manga/${book.id}`}>
|
||||
<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" }}
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import BookForm from "./BookForm";
|
||||
import BookList from "./BookList";
|
||||
|
||||
@@ -16,8 +15,30 @@ export default function Home({
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
statusFilter,
|
||||
setStatusFilter
|
||||
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>
|
||||
@@ -40,6 +61,18 @@ export default function Home({
|
||||
))}
|
||||
</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}
|
||||
@@ -53,6 +86,8 @@ export default function Home({
|
||||
statuses={statuses}
|
||||
onEdit={setEditingBook}
|
||||
onDelete={onDelete}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
5672
package-lock.json
generated
5672
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user