5 лаба
This commit is contained in:
29
Lab/src/components/BookCard.jsx
Normal file
29
Lab/src/components/BookCard.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import StatusLabel from "./StatusLabel";
|
||||
|
||||
function BookCard({ book, authorName, statusName, onEdit, onDelete }) {
|
||||
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={{ maxHeight: "300px", objectFit: "contain" }}
|
||||
/>
|
||||
<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)}>
|
||||
Редактировать
|
||||
</button>
|
||||
<button className="btn btn-danger" onClick={() => onDelete(book.id)}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookCard;
|
||||
121
Lab/src/components/BookForm.jsx
Normal file
121
Lab/src/components/BookForm.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const defaultForm = {
|
||||
title: "",
|
||||
description: "",
|
||||
authorId: "",
|
||||
statusId: "",
|
||||
cover: ""
|
||||
};
|
||||
|
||||
function BookForm({ authors, statuses, onSubmit, editingBook, onCancel }) {
|
||||
const [form, setForm] = useState(defaultForm);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingBook) setForm(editingBook);
|
||||
else setForm(defaultForm);
|
||||
}, [editingBook]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setForm((prev) => ({ ...prev, cover: reader.result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(form);
|
||||
setForm(defaultForm);
|
||||
if (onCancel) onCancel();
|
||||
};
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
setForm(defaultForm);
|
||||
if (onCancel) onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="bg-white p-4 rounded mb-4 shadow-sm">
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
className="form-control"
|
||||
name="title"
|
||||
placeholder="Название книги"
|
||||
value={form.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<select
|
||||
className="form-select"
|
||||
name="authorId"
|
||||
value={form.authorId}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите автора</option>
|
||||
{authors.map(a => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-12">
|
||||
<textarea
|
||||
className="form-control"
|
||||
name="description"
|
||||
placeholder="Описание"
|
||||
value={form.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<select
|
||||
className="form-select"
|
||||
name="statusId"
|
||||
value={form.statusId}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите статус</option>
|
||||
{statuses.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
className="form-control"
|
||||
type="file"
|
||||
name="coverFile"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 d-flex gap-2">
|
||||
<button className="btn btn-success" type="submit">
|
||||
{editingBook ? "Сохранить изменения" : "Добавить книгу"}
|
||||
</button>
|
||||
<button className="btn btn-secondary" type="button" onClick={handleCancel}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookForm;
|
||||
28
Lab/src/components/BookList.jsx
Normal file
28
Lab/src/components/BookList.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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 || "Нет статуса";
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center ">
|
||||
{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}
|
||||
>
|
||||
<BookCard
|
||||
book={book}
|
||||
authorName={getAuthorName(book.authorId)}
|
||||
statusName={getStatusName(book.statusId)}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookList;
|
||||
24
Lab/src/components/Footer.jsx
Normal file
24
Lab/src/components/Footer.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-custom-dark text-light text-center py-4 mt-5">
|
||||
<p>
|
||||
Спасибо, что посетили наш сайт, если возникли вопросы, обращайтесь к нам на почту{" "}
|
||||
<a href="mailto:manga@manga.scom" className="text-warning">
|
||||
manga@manga.scom
|
||||
</a>
|
||||
</p>
|
||||
<p>Если вас интересуют наши соц.сети, то вот они:</p>
|
||||
<div className="d-flex justify-content-center">
|
||||
<a href="https://vk.com/ded_moroz1509" className="me-3">
|
||||
<img src="/img/VK0.png" alt="VK" height="30" />
|
||||
</a>
|
||||
{/* Добавь другие соцсети по аналогии: */}
|
||||
{/* <a href="https://t.me/yourchannel" className="me-3">
|
||||
<img src="/img/telegram.png" alt="Telegram" height="30" />
|
||||
</a> */}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
58
Lab/src/components/Navbar.jsx
Normal file
58
Lab/src/components/Navbar.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg navbar-dark bg-custom-dark px-3">
|
||||
<Link className="navbar-brand" to="/">
|
||||
<img src="/img/manga.png" alt="ЛОГО" height="50" />
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNavDropdown"
|
||||
aria-controls="navbarNavDropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="Переключить навигацию"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div className="collapse navbar-collapse ms-3" id="navbarNavDropdown">
|
||||
<ul className="navbar-nav me-auto">
|
||||
<li className="nav-item dropdown">
|
||||
<a className="nav-link dropdown-toggle" href="#" id="catalogDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Каталог
|
||||
</a>
|
||||
<ul className="dropdown-menu dropdown-menu-dark" aria-labelledby="catalogDropdown">
|
||||
<li>
|
||||
<a className="dropdown-item" href="#">
|
||||
Жанр 1
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a className="dropdown-item" href="#">
|
||||
Жанр 2
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a className="dropdown-item" href="#">
|
||||
Жанр 3
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a className="dropdown-item" href="#">
|
||||
Жанр 4
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<Link to="/account" className="btn btn-outline-warning">
|
||||
Вход
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
13
Lab/src/components/StatusLabel.jsx
Normal file
13
Lab/src/components/StatusLabel.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
function StatusLabel({ statusName }) {
|
||||
let className = "fw-bold ";
|
||||
switch ((statusName || "").toLowerCase()) {
|
||||
case "вышла": className += "text-success"; break;
|
||||
case "не вышла": className += "text-danger"; break;
|
||||
case "не переведена": className += "text-warning"; break;
|
||||
case "прочитана": className += "text-primary"; break;
|
||||
default: className += "text-muted";
|
||||
}
|
||||
return <span className={className}>Статус: {statusName || "Неизвестен"}</span>
|
||||
}
|
||||
export default StatusLabel;
|
||||
|
||||
Reference in New Issue
Block a user