mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Исправление фильтров
This commit is contained in:
@@ -6,7 +6,7 @@ from sqlmodel import Session, select, col, func
|
|||||||
|
|
||||||
from library_service.auth import RequireAuth
|
from library_service.auth import RequireAuth
|
||||||
from library_service.settings import get_session
|
from library_service.settings import get_session
|
||||||
from library_service.models.db import Author, AuthorBookLink, Book
|
from library_service.models.db import Author, AuthorBookLink, Book, GenreBookLink
|
||||||
from library_service.models.dto import AuthorRead, BookCreate, BookList, BookRead, BookUpdate, GenreRead
|
from library_service.models.dto import AuthorRead, BookCreate, BookList, BookRead, BookUpdate, GenreRead
|
||||||
from library_service.models.dto.combined import (
|
from library_service.models.dto.combined import (
|
||||||
BookWithAuthorsAndGenres,
|
BookWithAuthorsAndGenres,
|
||||||
|
|||||||
+144
-88
@@ -1,4 +1,7 @@
|
|||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
let selectedAuthors = new Set();
|
||||||
|
let selectedGenres = new Set();
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch("/api/authors").then((response) => response.json()),
|
fetch("/api/authors").then((response) => response.json()),
|
||||||
fetch("/api/genres").then((response) => response.json()),
|
fetch("/api/genres").then((response) => response.json()),
|
||||||
@@ -6,111 +9,162 @@ $(document).ready(function () {
|
|||||||
.then(([authorsData, genresData]) => {
|
.then(([authorsData, genresData]) => {
|
||||||
const $dropdown = $("#author-dropdown");
|
const $dropdown = $("#author-dropdown");
|
||||||
authorsData.authors.forEach((author) => {
|
authorsData.authors.forEach((author) => {
|
||||||
const $div = $("<div>", {
|
$("<div>")
|
||||||
class: "p-2 hover:bg-gray-100 cursor-pointer",
|
.addClass("p-2 hover:bg-gray-100 cursor-pointer author-item")
|
||||||
"data-value": author.name,
|
.attr("data-value", author.name)
|
||||||
text: author.name,
|
.text(author.name)
|
||||||
});
|
.appendTo($dropdown);
|
||||||
$dropdown.append($div);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const $list = $("#genres-list");
|
const $list = $("#genres-list");
|
||||||
genresData.genres.forEach((genre) => {
|
genresData.genres.forEach((genre) => {
|
||||||
const $li = $("<li>", { class: "mb-1" });
|
$("<li>")
|
||||||
$li.html(`
|
.addClass("mb-1")
|
||||||
<label class="custom-checkbox flex items-center">
|
.html(
|
||||||
<input type="checkbox" />
|
`<label class="custom-checkbox flex items-center">
|
||||||
<span class="checkmark"></span>
|
<input type="checkbox" data-genre="${genre.name}" />
|
||||||
${genre.name}
|
<span class="checkmark"></span>
|
||||||
</label>
|
${genre.name}
|
||||||
`);
|
</label>`,
|
||||||
$list.append($li);
|
)
|
||||||
|
.appendTo($list);
|
||||||
});
|
});
|
||||||
|
|
||||||
initializeAuthorDropdown();
|
initializeAuthorDropdown();
|
||||||
|
initializeFilters();
|
||||||
})
|
})
|
||||||
.catch((error) => console.error("Error loading data:", error));
|
.catch((error) => console.error("Error loading data:", error));
|
||||||
|
|
||||||
function initializeAuthorDropdown() {
|
function initializeAuthorDropdown() {
|
||||||
const $authorSearchInput = $("#author-search-input");
|
const $input = $("#author-search-input");
|
||||||
const $authorDropdown = $("#author-dropdown");
|
const $dropdown = $("#author-dropdown");
|
||||||
const $selectedAuthorsContainer = $("#selected-authors-container");
|
const $container = $("#selected-authors-container");
|
||||||
const $dropdownItems = $authorDropdown.find("[data-value]");
|
|
||||||
let selectedAuthors = new Set();
|
|
||||||
|
|
||||||
const updateDropdownHighlights = () => {
|
function updateHighlights() {
|
||||||
$dropdownItems.each(function () {
|
$dropdown.find(".author-item").each(function () {
|
||||||
const value = $(this).data("value");
|
const value = $(this).attr("data-value");
|
||||||
$(this).toggleClass("bg-gray-200", selectedAuthors.has(value));
|
const isSelected = selectedAuthors.has(value);
|
||||||
|
$(this)
|
||||||
|
.toggleClass("bg-gray-300 text-gray-600", isSelected)
|
||||||
|
.toggleClass("hover:bg-gray-100", !isSelected);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
const renderSelectedAuthors = () => {
|
function filterDropdown(query) {
|
||||||
$selectedAuthorsContainer.children().not("#author-search-input").remove();
|
const lowerQuery = query.toLowerCase();
|
||||||
|
$dropdown.find(".author-item").each(function () {
|
||||||
|
$(this).toggle($(this).text().toLowerCase().includes(lowerQuery));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChips() {
|
||||||
|
$container.find(".author-chip").remove();
|
||||||
selectedAuthors.forEach((author) => {
|
selectedAuthors.forEach((author) => {
|
||||||
const $authorChip = $("<span>", {
|
$(`<span class="author-chip flex items-center bg-gray-500 text-white text-sm font-medium px-2.5 py-0.5 rounded-full">
|
||||||
class:
|
${author}
|
||||||
"flex items-center bg-gray-200 text-gray-800 text-sm font-medium px-2.5 py-0.5 rounded-full",
|
<button type="button" class="remove-author ml-1.5 inline-flex items-center p-0.5 text-gray-200 hover:text-white hover:bg-gray-600 rounded-full" data-author="${author}">
|
||||||
});
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 14 14">
|
||||||
$authorChip.html(`
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||||
${author}
|
</svg>
|
||||||
<button type="button" class="ml-1 inline-flex items-center p-0.5 text-sm text-gray-400 bg-transparent rounded-sm hover:bg-gray-200 hover:text-gray-900" data-author="${author}">
|
</button>
|
||||||
<svg class="w-2 h-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
</span>`).insertBefore($input);
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Remove author</span>
|
|
||||||
</button>
|
|
||||||
`);
|
|
||||||
$selectedAuthorsContainer.append($authorChip);
|
|
||||||
});
|
});
|
||||||
updateDropdownHighlights();
|
updateHighlights();
|
||||||
};
|
}
|
||||||
|
|
||||||
$authorSearchInput.on("focus", () => {
|
function toggleAuthor(author) {
|
||||||
$authorDropdown.removeClass("hidden");
|
if (selectedAuthors.has(author)) {
|
||||||
});
|
selectedAuthors.delete(author);
|
||||||
|
|
||||||
$authorSearchInput.on("input", function () {
|
|
||||||
const query = $(this).val().toLowerCase();
|
|
||||||
$dropdownItems.each(function () {
|
|
||||||
const text = $(this).text().toLowerCase();
|
|
||||||
$(this).css("display", text.includes(query) ? "block" : "none");
|
|
||||||
});
|
|
||||||
$authorDropdown.removeClass("hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on("click", function (event) {
|
|
||||||
if (
|
|
||||||
!$selectedAuthorsContainer.is(event.target) &&
|
|
||||||
!$selectedAuthorsContainer.has(event.target).length &&
|
|
||||||
!$authorDropdown.is(event.target) &&
|
|
||||||
!$authorDropdown.has(event.target).length
|
|
||||||
) {
|
|
||||||
$authorDropdown.addClass("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$authorDropdown.on("click", "[data-value]", function () {
|
|
||||||
const selectedValue = $(this).data("value");
|
|
||||||
if (selectedAuthors.has(selectedValue)) {
|
|
||||||
selectedAuthors.delete(selectedValue);
|
|
||||||
} else {
|
} else {
|
||||||
selectedAuthors.add(selectedValue);
|
selectedAuthors.add(author);
|
||||||
}
|
}
|
||||||
$authorSearchInput.val("");
|
$input.val("");
|
||||||
renderSelectedAuthors();
|
filterDropdown("");
|
||||||
$authorSearchInput.focus();
|
renderChips();
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.on("focus", () => $dropdown.removeClass("hidden"));
|
||||||
|
|
||||||
|
$input.on("input", function () {
|
||||||
|
filterDropdown($(this).val().toLowerCase());
|
||||||
|
$dropdown.removeClass("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
$selectedAuthorsContainer.on("click", "button", function () {
|
$(document).on("click", (e) => {
|
||||||
const authorToRemove = $(this).data("author");
|
if (
|
||||||
selectedAuthors.delete(authorToRemove);
|
!$(e.target).closest("#selected-authors-container, #author-dropdown")
|
||||||
renderSelectedAuthors();
|
.length
|
||||||
$authorSearchInput.focus();
|
) {
|
||||||
|
$dropdown.addClass("hidden");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
renderSelectedAuthors();
|
$dropdown.on("click", ".author-item", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleAuthor($(this).attr("data-value"));
|
||||||
|
$input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.on("click", ".remove-author", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedAuthors.delete($(this).attr("data-author"));
|
||||||
|
renderChips();
|
||||||
|
$input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.on("click", (e) => {
|
||||||
|
if (!$(e.target).closest(".author-chip").length) {
|
||||||
|
$input.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.renderAuthorChips = renderChips;
|
||||||
|
window.updateAuthorHighlights = updateHighlights;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFilters() {
|
||||||
|
const $bookSearch = $("#book-search-input");
|
||||||
|
const $applyBtn = $("#apply-filters-btn");
|
||||||
|
const $resetBtn = $("#reset-filters-btn");
|
||||||
|
|
||||||
|
$("#genres-list").on("change", "input[type='checkbox']", function () {
|
||||||
|
const genre = $(this).attr("data-genre");
|
||||||
|
if ($(this).is(":checked")) {
|
||||||
|
selectedGenres.add(genre);
|
||||||
|
} else {
|
||||||
|
selectedGenres.delete(genre);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$applyBtn.on("click", function () {
|
||||||
|
const searchQuery = $bookSearch.val().trim();
|
||||||
|
console.log("Применены фильтры:", {
|
||||||
|
search: searchQuery,
|
||||||
|
authors: Array.from(selectedAuthors),
|
||||||
|
genres: Array.from(selectedGenres),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$resetBtn.on("click", function () {
|
||||||
|
$bookSearch.val("");
|
||||||
|
|
||||||
|
selectedAuthors.clear();
|
||||||
|
$("#selected-authors-container .author-chip").remove();
|
||||||
|
if (window.updateAuthorHighlights) window.updateAuthorHighlights();
|
||||||
|
|
||||||
|
selectedGenres.clear();
|
||||||
|
$("#genres-list input[type='checkbox']").prop("checked", false);
|
||||||
|
|
||||||
|
console.log("Фильтры сброшены");
|
||||||
|
});
|
||||||
|
|
||||||
|
let searchTimeout;
|
||||||
|
$bookSearch.on("input", function () {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
console.log("Поиск:", $(this).val());
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const $guestLink = $("#guest-link");
|
const $guestLink = $("#guest-link");
|
||||||
@@ -182,11 +236,13 @@ $(document).ready(function () {
|
|||||||
function updateUserAvatar(email) {
|
function updateUserAvatar(email) {
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
const cleanEmail = email.trim().toLowerCase();
|
const cleanEmail = email.trim().toLowerCase();
|
||||||
const emailHash = sha256(cleanEmail);
|
const emailHash = sha256(cleanEmail);
|
||||||
|
|
||||||
const avatarUrl = `https://www.gravatar.com/avatar/${emailHash}?d=identicon&s=200`;
|
const avatarUrl = `https://www.gravatar.com/avatar/${emailHash}?d=identicon&s=200`;
|
||||||
const avatarImg = document.getElementById('user-avatar');
|
const avatarImg = document.getElementById("user-avatar");
|
||||||
if (avatarImg) { avatarImg.src = avatarUrl; }
|
if (avatarImg) {
|
||||||
|
avatarImg.src = avatarUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
@@ -204,9 +260,9 @@ $(document).ready(function () {
|
|||||||
.then((user) => {
|
.then((user) => {
|
||||||
showUser(user);
|
showUser(user);
|
||||||
updateUserAvatar(user.email);
|
updateUserAvatar(user.email);
|
||||||
|
|
||||||
document.getElementById('user-btn').classList.remove('hidden');
|
document.getElementById("user-btn").classList.remove("hidden");
|
||||||
document.getElementById('guest-link').classList.add('hidden');
|
document.getElementById("guest-link").classList.add("hidden");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
@@ -214,4 +270,4 @@ $(document).ready(function () {
|
|||||||
showGuest();
|
showGuest();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,66 +1,115 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}LiB - Главная{% endblock %} {% block
|
||||||
|
content %}
|
||||||
{% block title %}LiB - Главная{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex flex-1 mt-4 p-4">
|
<div class="flex flex-1 mt-4 p-4">
|
||||||
<aside class="w-1/4 bg-white p-4 rounded-lg shadow-md mr-4 h-fit resize-x overflow-auto min-w-64 max-w-96">
|
<aside
|
||||||
|
class="w-1/4 bg-white p-4 rounded-lg shadow-md mr-4 h-fit resize-x overflow-auto min-w-64 max-w-96"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Поиск</h2>
|
||||||
|
|
||||||
|
<div class="relative mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="book-search-input"
|
||||||
|
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-gray-500"
|
||||||
|
placeholder="Поиск книг..."
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mb-4">Фильтры</h2>
|
<h2 class="text-xl font-semibold mb-4">Фильтры</h2>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="font-medium mb-2">Авторы</h3>
|
<h3 class="font-medium mb-2">Авторы</h3>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="flex flex-wrap gap-2 p-2 border border-gray-300 rounded-md bg-white" id="selected-authors-container">
|
<div
|
||||||
<input type="text" id="author-search-input" class="flex-grow outline-none bg-transparent" placeholder="Начните вводить..." />
|
class="flex flex-wrap gap-2 p-2 border border-gray-300 rounded-md bg-white"
|
||||||
|
id="selected-authors-container"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="author-search-input"
|
||||||
|
class="flex-grow outline-none bg-transparent"
|
||||||
|
placeholder="Начните вводить..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="author-dropdown" class="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 hidden max-h-60 overflow-y-auto"></div>
|
<div
|
||||||
|
id="author-dropdown"
|
||||||
|
class="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 hidden max-h-60 overflow-y-auto"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="font-medium mb-2">Жанры</h3>
|
<h3 class="font-medium mb-2">Жанры</h3>
|
||||||
<ul id="genres-list"></ul>
|
<ul id="genres-list"></ul>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-full bg-gray-500 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition duration-200">
|
|
||||||
|
<button
|
||||||
|
id="apply-filters-btn"
|
||||||
|
class="w-full bg-gray-500 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition duration-200 mb-2"
|
||||||
|
>
|
||||||
Применить фильтры
|
Применить фильтры
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
id="reset-filters-btn"
|
||||||
|
class="w-full bg-white text-gray-500 py-2 px-4 rounded-lg border border-gray-300 hover:bg-gray-50 transition duration-200"
|
||||||
|
>
|
||||||
|
Сбросить фильтры
|
||||||
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="flex-1">
|
<main class="flex-1" id="books-container">
|
||||||
<div class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start">
|
<div
|
||||||
|
class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-bold mb-1">Product Title 1</h3>
|
<h3 class="text-lg font-bold mb-1">Product Title 1</h3>
|
||||||
<p class="text-gray-700 text-sm">
|
<p class="text-gray-700 text-sm">
|
||||||
A short description of the product, highlighting its
|
A short description of the product, highlighting its key
|
||||||
key features and benefits.
|
features and benefits.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-lg font-semibold text-gray-600">$29.99</span>
|
<span class="text-lg font-semibold text-gray-600">$29.99</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start">
|
<div
|
||||||
|
class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-bold mb-1">Product Title 2</h3>
|
<h3 class="text-lg font-bold mb-1">Product Title 2</h3>
|
||||||
<p class="text-gray-700 text-sm">
|
<p class="text-gray-700 text-sm">
|
||||||
Another great product with amazing features. You'll
|
Another great product with amazing features. You'll love it!
|
||||||
love it!
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-lg font-semibold text-blue-600">$49.99</span>
|
<span class="text-lg font-semibold text-blue-600">$49.99</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start">
|
<div
|
||||||
|
class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-bold mb-1">Product Title 3</h3>
|
<h3 class="text-lg font-bold mb-1">Product Title 3</h3>
|
||||||
<p class="text-gray-700 text-sm">
|
<p class="text-gray-700 text-sm">
|
||||||
This product is a must-have for every modern home.
|
This product is a must-have for every modern home. High
|
||||||
High quality and durable.
|
quality and durable.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-lg font-semibold text-gray-600">$19.99</span>
|
<span class="text-lg font-semibold text-gray-600">$19.99</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %} {% block scripts %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script type="text/javascript" src="/static/books.js"></script>
|
<script type="text/javascript" src="/static/books.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user