Расширение фронтэнда

This commit is contained in:
2025-12-22 01:38:52 +03:00
parent 5096b45243
commit 9d25d2e5de
35 changed files with 4901 additions and 288 deletions
+148 -26
View File
@@ -4,41 +4,31 @@ $(document).ready(() => {
label: "Доступна",
bgClass: "bg-green-100",
textClass: "text-green-800",
icon: `<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>`,
icon: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`,
},
borrowed: {
label: "Выдана",
bgClass: "bg-yellow-100",
textClass: "text-yellow-800",
icon: `<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>`,
icon: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`,
},
reserved: {
label: "Забронирована",
bgClass: "bg-blue-100",
textClass: "text-blue-800",
icon: `<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
</svg>`,
icon: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`,
},
restoration: {
label: "На реставрации",
bgClass: "bg-orange-100",
textClass: "text-orange-800",
icon: `<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path>
</svg>`,
icon: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>`,
},
written_off: {
label: "Списана",
bgClass: "bg-red-100",
textClass: "text-red-800",
icon: `<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
</svg>`,
icon: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path></svg>`,
},
};
@@ -55,6 +45,7 @@ $(document).ready(() => {
const pathParts = window.location.pathname.split("/");
const bookId = pathParts[pathParts.length - 1];
let currentBook = null;
if (!bookId || isNaN(bookId)) {
Utils.showToast("Некорректный ID книги", "error");
@@ -63,8 +54,14 @@ $(document).ready(() => {
Api.get(`/api/books/${bookId}`)
.then((book) => {
currentBook = book;
document.title = `LiB - ${book.title}`;
renderBook(book);
if (window.canManage) {
$("#edit-book-btn")
.attr("href", `/book/${book.id}/edit`)
.removeClass("hidden");
}
})
.catch((error) => {
console.error(error);
@@ -82,20 +79,19 @@ $(document).ready(() => {
);
$("#book-description").text(book.description || "Описание отсутствует");
const statusConfig = getStatusConfig(book.status);
$("#book-status")
.html(statusConfig.icon + statusConfig.label)
.removeClass()
.addClass(
`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${statusConfig.bgClass} ${statusConfig.textClass}`,
);
renderStatusWidget(book);
if (!window.canManage && book.status === "active") {
renderReserveButton();
}
if (book.genres && book.genres.length > 0) {
$("#genres-section").removeClass("hidden");
const $genres = $("#genres-container");
$genres.empty();
book.genres.forEach((g) => {
$genres.append(`
<a href="/books?genre_id=${g.id}" class="inline-flex items-center bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-full text-sm transition-colors">
<a href="/books?genre_id=${g.id}" class="inline-flex items-center bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-full text-sm transition-colors border border-gray-200">
${Utils.escapeHtml(g.name)}
</a>
`);
@@ -105,13 +101,14 @@ $(document).ready(() => {
if (book.authors && book.authors.length > 0) {
$("#authors-section").removeClass("hidden");
const $authors = $("#authors-container");
$authors.empty();
book.authors.forEach((a) => {
$authors.append(`
<a href="/author/${a.id}" class="flex items-center bg-gray-50 hover:bg-gray-100 rounded-lg p-2 border transition-colors">
<div class="w-8 h-8 bg-gray-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-2">
<a href="/author/${a.id}" class="flex items-center bg-white hover:bg-gray-50 rounded-lg p-2 border border-gray-200 shadow-sm transition-colors group">
<div class="w-8 h-8 bg-gray-200 text-gray-600 group-hover:bg-gray-300 rounded-full flex items-center justify-center text-sm font-bold mr-2 transition-colors">
${a.name.charAt(0).toUpperCase()}
</div>
<span class="text-gray-900 font-medium text-sm">${Utils.escapeHtml(a.name)}</span>
<span class="text-gray-800 font-medium text-sm">${Utils.escapeHtml(a.name)}</span>
</a>
`);
});
@@ -120,4 +117,129 @@ $(document).ready(() => {
$("#book-loader").addClass("hidden");
$("#book-content").removeClass("hidden");
}
function renderStatusWidget(book) {
const $container = $("#book-status-container");
$container.empty();
const config = getStatusConfig(book.status);
if (window.canManage) {
const $dropdownHTML = $(`
<div class="relative inline-block text-left w-full md:w-auto">
<button id="status-toggle-btn" type="button" class="w-full justify-center md:w-auto inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition-all shadow-sm ${config.bgClass} ${config.textClass} hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400">
${config.icon}
<span class="ml-2">${config.label}</span>
<svg class="ml-2 -mr-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="status-menu" class="hidden absolute left-0 md:left-1/2 md:-translate-x-1/2 mt-2 w-56 rounded-xl shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none overflow-hidden z-20">
<div class="py-1" role="menu">
${Object.entries(STATUS_CONFIG)
.map(
([key, conf]) => `
<button class="status-option w-full text-left px-4 py-2.5 text-sm hover:bg-gray-50 flex items-center gap-2 ${book.status === key ? "bg-gray-50 font-medium" : "text-gray-700"}"
data-status="${key}">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full ${conf.bgClass} ${conf.textClass}">
${conf.icon}
</span>
<span>${conf.label}</span>
${book.status === key ? '<svg class="ml-auto h-4 w-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>' : ""}
</button>
`,
)
.join("")}
</div>
</div>
</div>
`);
$container.append($dropdownHTML);
const $toggleBtn = $("#status-toggle-btn");
const $menu = $("#status-menu");
$toggleBtn.on("click", (e) => {
e.stopPropagation();
$menu.toggleClass("hidden");
});
$(document).on("click", (e) => {
if (
!$toggleBtn.is(e.target) &&
$toggleBtn.has(e.target).length === 0 &&
!$menu.has(e.target).length
) {
$menu.addClass("hidden");
}
});
$(".status-option").on("click", function () {
const newStatus = $(this).data("status");
if (newStatus !== currentBook.status) {
updateBookStatus(newStatus);
}
$menu.addClass("hidden");
});
} else {
$container.append(`
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-sm font-medium ${config.bgClass} ${config.textClass} shadow-sm">
${config.icon}
${config.label}
</span>
`);
}
}
function renderReserveButton() {
const $container = $("#book-actions-container");
$container.html(`
<button id="reserve-btn" class="w-full flex items-center justify-center px-4 py-2.5 bg-gray-800 text-white font-medium rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-all shadow-sm">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Зарезервировать
</button>
`);
$("#reserve-btn").on("click", function () {
Utils.showToast("Функция бронирования в разработке", "info");
});
}
async function updateBookStatus(newStatus) {
const $toggleBtn = $("#status-toggle-btn");
const originalContent = $toggleBtn.html();
$toggleBtn.prop("disabled", true).addClass("opacity-75").html(`
<svg class="animate-spin h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
Обновление...
`);
try {
const payload = {
title: currentBook.title,
description: currentBook.description,
status: newStatus,
};
const updatedBook = await Api.put(
`/api/books/${currentBook.id}`,
payload,
);
currentBook = updatedBook;
Utils.showToast("Статус успешно изменен", "success");
renderStatusWidget(updatedBook);
} catch (error) {
console.error(error);
Utils.showToast("Ошибка при смене статуса", "error");
$toggleBtn
.prop("disabled", false)
.removeClass("opacity-75")
.html(originalContent);
}
}
});