Files
PIBD-23_Ivanov.D.A._Interne…/MyWebSite/bookComponent.js
2025-05-17 11:41:06 +04:00

795 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
// Модель
class BookModel {
constructor() {
this.API_URL = "http://localhost:3001";
}
async fetchGenres() {
try {
const response = await fetch(`${this.API_URL}/genres`);
if (!response.ok) throw new Error("Ошибка загрузки жанров");
return await response.json();
} catch (error) {
console.error("fetchGenres error:", error);
return [];
}
}
async createGenre(genreData) {
try {
const response = await fetch(`${this.API_URL}/genres`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(genreData),
});
if (!response.ok) throw new Error("Ошибка создания жанра");
return await response.json();
} catch (error) {
console.error("createGenre error:", error);
throw error;
}
}
async fetchBooksByGenre(genreId) {
try {
const response = await fetch(`${this.API_URL}/books?genreId=${genreId}`);
if (!response.ok) throw new Error("Ошибка загрузки книг");
return await response.json();
} catch (error) {
console.error("fetchBooksByGenre error:", error);
return [];
}
}
async fetchBook(id) {
try {
const response = await fetch(`${this.API_URL}/books/${id}`);
if (!response.ok) throw new Error("Ошибка загрузки книги");
return await response.json();
} catch (error) {
console.error("fetchBook error:", error);
return null;
}
}
async createBook(bookData) {
try {
const response = await fetch(`${this.API_URL}/books`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(bookData),
});
if (!response.ok) throw new Error("Ошибка создания книги");
return await response.json();
} catch (error) {
console.error("createBook error:", error);
throw error;
}
}
async updateBook(id, bookData) {
try {
const response = await fetch(`${this.API_URL}/books/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(bookData),
});
if (!response.ok) throw new Error("Ошибка обновления книги");
return await response.json();
} catch (error) {
console.error("updateBook error:", error);
throw error;
}
}
async deleteBook(id) {
try {
const response = await fetch(`${this.API_URL}/books/${id}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Ошибка удаления книги");
return await response.json();
} catch (error) {
console.error("deleteBook error:", error);
throw error;
}
}
async fetchCartItems() {
try {
const response = await fetch(`${this.API_URL}/cart?_expand=book`);
if (!response.ok) throw new Error("Ошибка загрузки корзины");
return await response.json();
} catch (error) {
console.error("fetchCartItems error:", error);
return [];
}
}
async addToCart(bookId) {
try {
const existingItem = await this.getCartItemByBookId(bookId);
if (existingItem) {
return await this.updateCartItem(existingItem.id, { quantity: existingItem.quantity + 1 });
} else {
const response = await fetch(`${this.API_URL}/cart`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ bookId, quantity: 1 }),
});
if (!response.ok) throw new Error("Ошибка добавления в корзину");
return await response.json();
}
} catch (error) {
console.error("addToCart error:", error);
throw error;
}
}
async getCartItemByBookId(bookId) {
try {
const response = await fetch(`${this.API_URL}/cart?bookId=${bookId}`);
if (!response.ok) throw new Error("Ошибка проверки корзины");
const items = await response.json();
return items[0] || null;
} catch (error) {
console.error("getCartItemByBookId error:", error);
return null;
}
}
async updateCartItem(id, data) {
try {
const response = await fetch(`${this.API_URL}/cart/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) throw new Error("Ошибка обновления корзины");
return await response.json();
} catch (error) {
console.error("updateCartItem error:", error);
throw error;
}
}
async removeFromCart(id) {
try {
const response = await fetch(`${this.API_URL}/cart/${id}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Ошибка удаления из корзины");
return await response.json();
} catch (error) {
console.error("removeFromCart error:", error);
throw error;
}
}
async clearCart() {
try {
const items = await this.fetchCartItems();
await Promise.all(items.map((item) => this.removeFromCart(item.id)));
return true;
} catch (error) {
console.error("clearCart error:", error);
throw error;
}
}
}
// Представление
class BookView {
constructor() {
this.bookModal = document.getElementById("bookModal")
? new bootstrap.Modal(document.getElementById("bookModal"))
: null;
this.bookModalTitle = document.getElementById("bookModalLabel");
this.bookForm = document.getElementById("bookForm");
this.genreSelect = document.getElementById("bookGenre");
this.genreModal = document.getElementById("genreModal")
? new bootstrap.Modal(document.getElementById("genreModal"))
: null;
this.genreModalTitle = document.getElementById("genreModalLabel");
this.genreForm = document.getElementById("genreForm");
this.cartModal = document.getElementById("cartModal")
? new bootstrap.Modal(document.getElementById("cartModal"))
: null;
// Ищем контейнеры корзины на разных страницах
this.cartItemsContainer = document.getElementById("cartItemsContainer") || document.getElementById("cartItems");
this.cartTotal = document.getElementById("cartTotal") || document.querySelector("#cartModal #cartTotal");
// Инициализация refs для модальных окон
this.modalRefs = {
bookModal: React.createRef(),
genreModal: React.createRef(),
cartModal: React.createRef(),
};
}
renderBooks(books, genreName) {
const container = document.getElementById("books-container");
if (!container) return;
container = document.getElementById(`${genreName.toLowerCase()}-books`);
if (!container) {
console.warn(`Контейнер для жанра ${genreName} не найден`);
return;
}
container.innerHTML = "";
if (!books || books.length === 0) {
container.innerHTML = '<div class="col-12 text-muted">Книги не найдены</div>';
return;
}
books.forEach((book) => {
const bookCard = this.createBookCard(book);
container.appendChild(bookCard);
});
}
renderGenresSections(genres) {
const main = document.querySelector("main");
if (!main) return;
// Удаляем старые секции жанров
document.querySelectorAll(".genre-section").forEach((section) => section.remove());
// Создаём новые секции для каждого жанра
genres.forEach((genre) => {
const section = document.createElement("section");
section.className = "mb-5 genre-section";
section.innerHTML = `
<div class="genre-title bg-light p-3 rounded text-center mb-4">
<h3>${genre.name}</h3>
</div>
<div class="row g-4" id="${genre.name.toLowerCase()}-books"></div>
`;
main.insertBefore(section, main.querySelector("footer"));
});
}
createBookCard(book) {
const col = document.createElement("div");
col.className = "col-md-6 mb-4";
col.innerHTML = `
<div class="card h-100 border-0 shadow-sm" data-id="${book.id}">
<div class="row g-0">
<div class="col-md-4">
<img src="${book.image || "images/default-book.jpg"}"
class="img-fluid rounded-start h-100"
alt="${book.title || "Без названия"}"
style="object-fit: cover"
onerror="this.src='images/default-book.jpg'">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${book.title || "Без названия"}</h5>
<p class="card-text text-muted">${book.author || "Автор не указан"}</p>
<p class="card-text">${book.description || "Описание отсутствует"}</p>
<div class="d-flex justify-content-between align-items-center">
<p class="h5 mb-0">${book.price || 0} руб.</p>
<div>
<button class="btn btn-primary me-2 mb-2 add-to-cart">В корзину</button>
<button class="btn btn-outline-secondary me-2 mb-2 edit-book">Редактировать</button>
<button class="btn btn-outline-danger delete-book">Удалить</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
return col;
}
renderGenreSelect(genres) {
if (!this.genreSelect) {
console.error("Элемент выбора жанра не найден");
return;
}
this.genreSelect.innerHTML = (genres || [])
.map((genre) => `<option value="${genre.id}">${genre.name}</option>`)
.join("");
}
renderCart(cartItems) {
if (!this.cartItemsContainer || !this.cartTotal) {
console.error("Элементы корзины не найдены");
return;
}
this.cartItemsContainer.innerHTML = "";
let total = 0;
if (!cartItems || cartItems.length === 0) {
this.cartItemsContainer.innerHTML = '<p class="text-muted">Корзина пуста</p>';
this.cartTotal.textContent = "0 руб.";
return;
}
cartItems.forEach((item) => {
if (!item.book) return;
const book = item.book;
const itemTotal = (book.price || 0) * (item.quantity || 1);
total += itemTotal;
const cartItem = document.createElement("div");
cartItem.className = "card mb-3";
cartItem.innerHTML = `
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-2">
<img src="${book.image || "images/default-book.jpg"}"
alt="${book.title || "Без названия"}"
class="img-fluid rounded"
onerror="this.src='images/default-book.jpg'">
</div>
<div class="col-md-6">
<h5>${book.title || "Без названия"}</h5>
<p class="text-muted">${book.author || "Автор не указан"}</p>
<p>Цена: ${book.price || 0} руб. × ${item.quantity || 1} = ${itemTotal} руб.</p>
</div>
<div class="col-md-2">
<input type="number" min="1" value="${item.quantity || 1}"
class="form-control cart-item-quantity"
data-id="${item.id}">
</div>
<div class="col-md-2 text-center">
<button class="btn btn-outline-danger remove-from-cart" data-id="${item.id}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`;
this.cartItemsContainer.appendChild(cartItem);
});
this.cartTotal.textContent = `${total} руб.`;
}
showBookModal(title, bookData = null) {
if (!this.bookModal || !this.bookModalTitle || !this.bookForm) {
console.error("Элементы модального окна книги не найдены");
return;
}
this.bookModalTitle.textContent = title || "Книга";
if (bookData) {
document.getElementById("bookId").value = bookData.id || "";
document.getElementById("bookTitle").value = bookData.title || "";
document.getElementById("bookAuthor").value = bookData.author || "";
document.getElementById("bookPrice").value = bookData.price || "";
document.getElementById("bookDescription").value = bookData.description || "";
document.getElementById("bookImage").value = (bookData.image || "").replace("images/", "");
document.getElementById("bookGenre").value = bookData.genreId || "";
} else {
this.bookForm.reset();
document.getElementById("bookId").value = "";
}
this.bookModal.show();
}
showCartModal() {
if (this.cartModal) {
this.cartModal.show();
} else {
console.error("Модальное окно корзины не найдено");
}
}
bindAddBook(handler) {
document.querySelectorAll(".add-book-btn").forEach((btn) => {
btn.addEventListener("click", handler);
});
}
bindEditBook(handler) {
document.addEventListener("click", (event) => {
if (event.target.classList.contains("edit-book")) {
const card = event.target.closest(".card");
if (card) {
const id = parseInt(card.dataset.id);
if (!isNaN(id)) handler(id);
}
}
});
}
bindDeleteBook(handler) {
document.addEventListener("click", (event) => {
if (event.target.classList.contains("delete-book")) {
const card = event.target.closest(".card");
if (card) {
const id = parseInt(card.dataset.id);
if (!isNaN(id) && confirm("Вы уверены, что хотите удалить эту книгу?")) {
handler(id);
}
}
}
});
}
bindAddToCart(handler) {
document.addEventListener("click", (event) => {
if (event.target.classList.contains("add-to-cart")) {
const card = event.target.closest(".card");
if (card) {
const id = parseInt(card.dataset.id);
if (!isNaN(id)) handler(id);
}
}
});
}
bindShowCart(handler) {
document.querySelectorAll(".show-cart-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
handler();
});
});
}
bindRemoveFromCart(handler) {
document.addEventListener("click", (event) => {
const removeBtn = event.target.closest(".remove-from-cart");
if (removeBtn) {
const id = parseInt(removeBtn.dataset.id);
if (!isNaN(id)) handler(id);
}
});
}
bindUpdateCartItem(handler) {
document.addEventListener("change", (event) => {
if (event.target.classList.contains("cart-item-quantity")) {
const id = parseInt(event.target.dataset.id);
const quantity = parseInt(event.target.value);
if (!isNaN(id) && !isNaN(quantity) && quantity > 0) {
handler(id, quantity);
} else {
event.target.value = 1;
}
}
});
}
bindClearCart(handler) {
const clearBtn = document.getElementById("clearCartBtn");
if (clearBtn) {
clearBtn.addEventListener("click", async (e) => {
e.preventDefault();
if (confirm("Вы уверены, что хотите очистить корзину?")) {
await handler();
// После очистки обновляем отображение
const cartItems = await this.model.fetchCartItems();
this.renderCart(cartItems);
await this.controller.updateCartCount();
}
});
}
}
bindCheckout(handler) {
const checkoutBtn = document.getElementById("checkoutBtn");
if (checkoutBtn) {
checkoutBtn.addEventListener("click", handler);
}
}
bindSubmitBookForm(handler) {
if (this.bookForm) {
this.bookForm.addEventListener("submit", (event) => {
event.preventDefault();
const id = document.getElementById("bookId").value;
const bookData = {
title: document.getElementById("bookTitle").value,
author: document.getElementById("bookAuthor").value,
price: parseInt(document.getElementById("bookPrice").value) || 0,
description: document.getElementById("bookDescription").value,
image: document.getElementById("bookImage").value.startsWith("http")
? document.getElementById("bookImage").value
: `images/${document.getElementById("bookImage").value}`,
genreId: parseInt(document.getElementById("bookGenre").value),
};
if (!bookData.title || !bookData.author) {
alert("Пожалуйста, заполните обязательные поля");
return;
}
handler(id, bookData);
});
}
}
// Метод для показа модального окна жанра
showGenreModal(title) {
if (!this.genreModal || !this.genreModalTitle || !this.genreForm) {
console.error("Элементы модального окна жанра не найдены");
return;
}
this.genreModalTitle.textContent = title || "Жанр";
this.genreForm.reset();
this.genreModal.show();
}
// Привязка обработчика добавления жанра
bindAddGenre(handler) {
document.querySelectorAll(".add-genre-btn").forEach((btn) => {
btn.addEventListener("click", handler);
});
}
// Привязка обработчика отправки формы жанра
bindSubmitGenreForm(handler) {
if (this.genreForm) {
this.genreForm.addEventListener("submit", (event) => {
event.preventDefault();
const genreData = {
name: document.getElementById("genreName").value,
};
if (!genreData.name) {
alert("Пожалуйста, укажите название жанра");
return;
}
handler(genreData);
});
}
}
}
// Контроллер
class BookController {
constructor(model, view) {
this.model = model;
this.view = view;
this.view.controller = this;
this.init();
}
async init() {
try {
// Инициализация обработчиков событий
this.view.bindAddGenre(() => this.handleAddGenre());
this.view.bindSubmitGenreForm((genreData) => this.handleSubmitGenreForm(genreData));
this.view.bindAddBook(() => this.handleAddBook());
this.view.bindEditBook((id) => this.handleEditBook(id));
this.view.bindDeleteBook((id) => this.handleDeleteBook(id));
this.view.bindAddToCart((id) => this.handleAddToCart(id));
this.view.bindShowCart(() => this.handleShowCart());
this.view.bindRemoveFromCart((id) => this.handleRemoveFromCart(id));
this.view.bindUpdateCartItem((id, quantity) => this.handleUpdateCartItem(id, quantity));
this.view.bindClearCart(() => this.handleClearCart());
this.view.bindCheckout(() => this.handleCheckout());
this.view.bindSubmitBookForm((id, bookData) => this.handleSubmitBookForm(id, bookData));
// Загрузка данных только если мы на странице каталога
if (document.getElementById("fantasy-books")) {
await this.loadData();
}
// Обновляем счетчик корзины всегда
await this.updateCartCount();
// Если мы на странице корзины, загружаем ее содержимое
if (document.getElementById("cartItemsContainer")) {
const cartItems = await this.model.fetchCartItems();
this.view.renderCart(cartItems);
}
} catch (error) {
console.error("Ошибка инициализации:", error);
}
}
// Новые методы обработчиков:
async handleAddGenre() {
this.view.showGenreModal("Добавить новый жанр");
}
async handleSubmitGenreForm(genreData) {
try {
const newGenre = await this.model.createGenre(genreData);
const genres = await this.model.fetchGenres();
// Обновляем интерфейс
this.view.renderGenreSelect(genres);
this.view.renderGenresSections(genres);
// Загружаем книги для ВСЕХ жанров после обновления секций
for (const genre of genres) {
const books = await this.model.fetchBooksByGenre(genre.id);
this.view.renderBooks(books, genre.name);
}
if (this.view.genreModal) {
this.view.genreModal.hide();
}
alert("Жанр успешно добавлен!");
} catch (error) {
console.error("Ошибка сохранения жанра:", error);
alert("Не удалось сохранить жанр");
}
}
async loadData() {
try {
const genres = await this.model.fetchGenres();
this.view.renderGenreSelect(genres);
this.view.renderGenresSections(genres);
for (const genre of genres) {
const books = await this.model.fetchBooksByGenre(genre.id);
this.view.renderBooks(books, genre.name);
}
} catch (error) {
console.error("Ошибка загрузки данных:", error);
}
}
async handleAddBook() {
this.view.showBookModal("Добавить новую книгу");
}
async handleEditBook(id) {
try {
const book = await this.model.fetchBook(id);
if (book) {
this.view.showBookModal("Редактировать книгу", book);
} else {
alert("Книга не найдена");
}
} catch (error) {
console.error("Ошибка редактирования книги:", error);
alert("Не удалось загрузить данные книги");
}
}
async handleDeleteBook(id) {
try {
await this.model.deleteBook(id);
location.reload();
} catch (error) {
console.error("Ошибка удаления книги:", error);
alert("Не удалось удалить книгу");
}
}
async handleAddToCart(bookId) {
try {
await this.model.addToCart(bookId);
await this.updateCartCount();
alert("Книга добавлена в корзину");
} catch (error) {
console.error("Ошибка добавления в корзину:", error);
alert("Не удалось добавить книгу в корзину");
}
}
async handleShowCart() {
try {
const cartItems = await this.model.fetchCartItems();
this.view.renderCart(cartItems);
this.view.showCartModal();
} catch (error) {
console.error("Ошибка загрузки корзины:", error);
alert("Не удалось загрузить корзину");
}
}
async handleRemoveFromCart(id) {
try {
await this.model.removeFromCart(id);
const cartItems = await this.model.fetchCartItems();
this.view.renderCart(cartItems);
await this.updateCartCount();
} catch (error) {
console.error("Ошибка удаления из корзины:", error);
alert("Не удалось удалить товар из корзины");
}
}
async handleUpdateCartItem(id, quantity) {
try {
await this.model.updateCartItem(id, { quantity });
const cartItems = await this.model.fetchCartItems();
this.view.renderCart(cartItems);
await this.updateCartCount();
} catch (error) {
console.error("Ошибка обновления корзины:", error);
alert("Не удалось обновить количество товара");
}
}
async handleClearCart() {
try {
await this.model.clearCart();
// Обновляем отображение корзины
const cartItems = await this.model.fetchCartItems();
this.view.renderCart(cartItems);
await this.updateCartCount();
// Показываем сообщение об успехе
alert("Корзина успешно очищена");
} catch (error) {
console.error("Ошибка очистки корзины:", error);
alert("Не удалось очистить корзину");
}
}
async handleCheckout() {
try {
alert("Заказ оформлен! Спасибо за покупку!");
await this.model.clearCart();
this.view.renderCart([]);
await this.updateCartCount();
} catch (error) {
console.error("Ошибка оформления заказа:", error);
alert("Не удалось оформить заказ");
}
}
async handleSubmitBookForm(id, bookData) {
try {
if (id) {
await this.model.updateBook(id, bookData);
} else {
await this.model.createBook(bookData);
}
location.reload();
} catch (error) {
console.error("Ошибка сохранения книги:", error);
alert("Не удалось сохранить книгу");
}
}
async updateCartCount() {
try {
const cartItems = await this.model.fetchCartItems();
const totalItems = cartItems.reduce((sum, item) => sum + (item.quantity || 0), 0);
document.querySelectorAll(".cart-count").forEach((el) => {
el.textContent = totalItems;
el.style.display = totalItems > 0 ? "inline-block" : "none";
});
} catch (error) {
console.error("Ошибка обновления счетчика корзины:", error);
}
}
}
// Инициализация приложения
document.addEventListener("DOMContentLoaded", () => {
try {
const model = new BookModel();
const view = new BookView();
new BookController(model, view);
} catch (error) {
console.error("Ошибка инициализации приложения:", error);
}
});