Исправление фильтров

This commit is contained in:
2025-12-20 00:39:48 +03:00
parent 345ff8f23f
commit 85b531e6e1
3 changed files with 218 additions and 113 deletions
+1 -1
View File
@@ -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
View File
@@ -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();
}); });
} }
}); });
+73 -24
View File
@@ -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 %}