Добавление главной страницы о общей статистикой

This commit is contained in:
2025-12-20 01:18:47 +03:00
parent 64a46645c5
commit 961bf95af7
7 changed files with 800 additions and 122 deletions
+2 -2
View File
@@ -33,7 +33,7 @@ def get_info(app) -> Dict:
@router.get("/", include_in_schema=False) @router.get("/", include_in_schema=False)
async def root(request: Request, app=Depends(lambda: get_app())): async def root(request: Request, app=Depends(lambda: get_app())):
"""Эндпоинт главной страницы""" """Эндпоинт главной страницы"""
return RedirectResponse("/books") return templates.TemplateResponse(request, "index.html", get_info(app))
@router.get("/books", include_in_schema=False) @router.get("/books", include_in_schema=False)
@@ -85,7 +85,7 @@ async def api_info(app=Depends(lambda: get_app())):
description="Возвращает статистическую информацию о системе", description="Возвращает статистическую информацию о системе",
) )
async def api_stats(session: Session = Depends(get_session)): async def api_stats(session: Session = Depends(get_session)):
"""Эндпоинт стстистика системы""" """Эндпоинт стстистики системы"""
authors = select(func.count()).select_from(Author) authors = select(func.count()).select_from(Author)
books = select(func.count()).select_from(Book) books = select(func.count()).select_from(Book)
genres = select(func.count()).select_from(Genre) genres = select(func.count()).select_from(Genre)
+13 -7
View File
@@ -1,9 +1,15 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <?xml version="1.0" encoding="utf-8" ?>
<svg width="800px" height="800px" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
width="800px"
height="800px"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d="M0.877014 7.49988C0.877014 3.84219 3.84216 0.877045 7.49985 0.877045C11.1575 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1575 14.1227 7.49985 14.1227C3.84216 14.1227 0.877014 11.1575 0.877014 7.49988ZM7.49985 1.82704C4.36683 1.82704 1.82701 4.36686 1.82701 7.49988C1.82701 8.97196 2.38774 10.3131 3.30727 11.3213C4.19074 9.94119 5.73818 9.02499 7.50023 9.02499C9.26206 9.02499 10.8093 9.94097 11.6929 11.3208C12.6121 10.3127 13.1727 8.97172 13.1727 7.49988C13.1727 4.36686 10.6328 1.82704 7.49985 1.82704ZM10.9818 11.9787C10.2839 10.7795 8.9857 9.97499 7.50023 9.97499C6.01458 9.97499 4.71624 10.7797 4.01845 11.9791C4.97952 12.7272 6.18765 13.1727 7.49985 13.1727C8.81227 13.1727 10.0206 12.727 10.9818 11.9787ZM5.14999 6.50487C5.14999 5.207 6.20212 4.15487 7.49999 4.15487C8.79786 4.15487 9.84999 5.207 9.84999 6.50487C9.84999 7.80274 8.79786 8.85487 7.49999 8.85487C6.20212 8.85487 5.14999 7.80274 5.14999 6.50487ZM7.49999 5.10487C6.72679 5.10487 6.09999 5.73167 6.09999 6.50487C6.09999 7.27807 6.72679 7.90487 7.49999 7.90487C8.27319 7.90487 8.89999 7.27807 8.89999 6.50487C8.89999 5.73167 8.27319 5.10487 7.49999 5.10487Z" d="M0.877014 7.49988C0.877014 3.84219 3.84216 0.877045 7.49985 0.877045C11.1575 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1575 14.1227 7.49985 14.1227C3.84216 14.1227 0.877014 11.1575 0.877014 7.49988ZM7.49985 1.82704C4.36683 1.82704 1.82701 4.36686 1.82701 7.49988C1.82701 8.97196 2.38774 10.3131 3.30727 11.3213C4.19074 9.94119 5.73818 9.02499 7.50023 9.02499C9.26206 9.02499 10.8093 9.94097 11.6929 11.3208C12.6121 10.3127 13.1727 8.97172 13.1727 7.49988C13.1727 4.36686 10.6328 1.82704 7.49985 1.82704ZM10.9818 11.9787C10.2839 10.7795 8.9857 9.97499 7.50023 9.97499C6.01458 9.97499 4.71624 10.7797 4.01845 11.9791C4.97952 12.7272 6.18765 13.1727 7.49985 13.1727C8.81227 13.1727 10.0206 12.727 10.9818 11.9787ZM5.14999 6.50487C5.14999 5.207 6.20212 4.15487 7.49999 4.15487C8.79786 4.15487 9.84999 5.207 9.84999 6.50487C9.84999 7.80274 8.79786 8.85487 7.49999 8.85487C6.20212 8.85487 5.14999 7.80274 5.14999 6.50487ZM7.49999 5.10487C6.72679 5.10487 6.09999 5.73167 6.09999 6.50487C6.09999 7.27807 6.72679 7.90487 7.49999 7.90487C8.27319 7.90487 8.89999 7.27807 8.89999 6.50487C8.89999 5.73167 8.27319 5.10487 7.49999 5.10487Z"
fill="#000000" fill="#000000"
/> />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+274
View File
@@ -0,0 +1,274 @@
const svg = document.getElementById("bookSvg");
const NS = "http://www.w3.org/2000/svg";
const svgWidth = 200;
const svgHeight = 250;
const lineCount = 5;
const lineDelay = 16;
const bookWidth = 120;
const bookHeight = 180;
const bookX = (svgWidth - bookWidth) / 2;
const bookY = (svgHeight - bookHeight) / 2;
const desiredLineSpacing = 8;
const baseLineWidth = 2;
const maxLineWidth = 10;
const maxLineHeight = bookHeight - 24;
const innerPaddingX = 10;
const appearStagger = 8;
let lineSpacing;
if (lineCount > 1) {
const maxSpan = Math.max(0, bookWidth - maxLineWidth - 2 * innerPaddingX);
const wishSpan = desiredLineSpacing * (lineCount - 1);
const realSpan = Math.min(wishSpan, maxSpan);
lineSpacing = realSpan / (lineCount - 1);
} else {
lineSpacing = 0;
}
const linesSpan = lineSpacing * (lineCount - 1);
const rightBase = bookX + bookWidth - innerPaddingX - maxLineWidth;
const lineStartX = rightBase - linesSpan;
const leftLimit = bookX + innerPaddingX;
let phase = 0;
let time = 0;
const baseAppearDuration = 40;
const appearDuration = baseAppearDuration + (lineCount - 1) * appearStagger;
const baseFlipDuration = 120;
const flipDuration = baseFlipDuration + (lineCount - 1) * lineDelay;
const baseDisappearDuration = 40;
const disappearDuration =
baseDisappearDuration + (lineCount - 1) * appearStagger;
const pauseDuration = 30;
const book = document.createElementNS(NS, "rect");
book.setAttribute("x", bookX);
book.setAttribute("y", bookY);
book.setAttribute("width", bookWidth);
book.setAttribute("height", bookHeight);
book.setAttribute("fill", "#374151");
book.setAttribute("rx", "4");
svg.appendChild(book);
const lines = [];
for (let i = 0; i < lineCount; i++) {
const line = document.createElementNS(NS, "rect");
line.setAttribute("fill", "#ffffff");
line.setAttribute("rx", "1");
svg.appendChild(line);
const baseX = lineStartX + i * lineSpacing;
const targetX = leftLimit + i * lineSpacing;
const moveDistance = baseX - targetX;
lines.push({
el: line,
baseX,
targetX,
moveDistance,
currentX: baseX,
width: baseLineWidth,
height: 0,
});
}
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
function easeOutQuad(t) {
return 1 - (1 - t) * (1 - t);
}
function easeInQuad(t) {
return t * t;
}
function updateLine(line) {
const el = line.el;
const centerY = bookY + bookHeight / 2;
el.setAttribute("x", line.currentX);
el.setAttribute("y", centerY - line.height / 2);
el.setAttribute("width", line.width);
el.setAttribute("height", Math.max(0, line.height));
}
function animateBook() {
time++;
if (phase === 0) {
for (let i = 0; i < lineCount; i++) {
const delay = (lineCount - 1 - i) * appearStagger;
const localTime = Math.max(0, time - delay);
const progress = Math.min(1, localTime / baseAppearDuration);
const easedProgress = easeOutQuad(progress);
lines[i].height = maxLineHeight * easedProgress;
lines[i].currentX = lines[i].baseX;
lines[i].width = baseLineWidth;
updateLine(lines[i]);
}
if (time >= appearDuration) {
phase = 1;
time = 0;
}
} else if (phase === 1) {
for (let i = 0; i < lineCount; i++) {
const delay = i * lineDelay;
const localTime = Math.max(0, time - delay);
const progress = Math.min(1, localTime / baseFlipDuration);
const moveProgress = easeInOutQuad(progress);
lines[i].currentX = lines[i].baseX - lines[i].moveDistance * moveProgress;
const widthProgress =
progress < 0.5
? easeOutQuad(progress * 2)
: 1 - easeInQuad((progress - 0.5) * 2);
lines[i].width =
baseLineWidth + (maxLineWidth - baseLineWidth) * widthProgress;
updateLine(lines[i]);
}
if (time >= flipDuration) {
phase = 2;
time = 0;
}
} else if (phase === 2) {
for (let i = 0; i < lineCount; i++) {
const delay = (lineCount - 1 - i) * appearStagger;
const localTime = Math.max(0, time - delay);
const progress = Math.min(1, localTime / baseDisappearDuration);
const easedProgress = easeInQuad(progress);
lines[i].height = maxLineHeight * (1 - easedProgress);
updateLine(lines[i]);
}
if (time >= disappearDuration + pauseDuration) {
phase = 0;
time = 0;
for (let i = 0; i < lineCount; i++) {
lines[i].currentX = lines[i].baseX;
lines[i].width = baseLineWidth;
lines[i].height = 0;
}
}
}
requestAnimationFrame(animateBook);
}
animateBook();
function animateCounter(element, target, duration = 2000) {
const start = 0;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = 1 - Math.pow(1 - progress, 3);
const current = Math.floor(start + (target - start) * easedProgress);
element.textContent = current.toLocaleString("ru-RU");
if (progress < 1) {
requestAnimationFrame(update);
} else {
element.textContent = target.toLocaleString("ru-RU");
}
}
requestAnimationFrame(update);
}
async function loadStats() {
try {
const response = await fetch("/api/stats");
if (!response.ok) {
throw new Error("Ошибка загрузки статистики");
}
const stats = await response.json();
setTimeout(() => {
const booksEl = document.getElementById("stat-books");
const authorsEl = document.getElementById("stat-authors");
const genresEl = document.getElementById("stat-genres");
const usersEl = document.getElementById("stat-users");
if (booksEl) {
animateCounter(booksEl, stats.books, 1500);
}
setTimeout(() => {
if (authorsEl) {
animateCounter(authorsEl, stats.authors, 1500);
}
}, 150);
setTimeout(() => {
if (genresEl) {
animateCounter(genresEl, stats.genres, 1500);
}
}, 300);
setTimeout(() => {
if (usersEl) {
animateCounter(usersEl, stats.users, 1500);
}
}, 450);
}, 500);
} catch (error) {
console.error("Ошибка загрузки статистики:", error);
document.getElementById("stat-books").textContent = "—";
document.getElementById("stat-authors").textContent = "—";
document.getElementById("stat-genres").textContent = "—";
document.getElementById("stat-users").textContent = "—";
}
}
function observeStatCards() {
const cards = document.querySelectorAll(".stat-card");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add("animate-fade-in");
entry.target.style.opacity = "1";
entry.target.style.transform = "translateY(0)";
}, index * 100);
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 },
);
cards.forEach((card) => {
card.style.opacity = "0";
card.style.transform = "translateY(20px)";
card.style.transition = "opacity 0.5s ease, transform 0.5s ease";
observer.observe(card);
});
}
document.addEventListener("DOMContentLoaded", () => {
loadStats();
observeStatCards();
});
+91 -11
View File
@@ -19,7 +19,6 @@ nav ul li a {
font-size: large; font-size: large;
} }
/* Custom checkbox styles */
.custom-checkbox { .custom-checkbox {
display: inline-block; display: inline-block;
position: relative; position: relative;
@@ -40,7 +39,7 @@ nav ul li a {
height: 18px; height: 18px;
width: 18px; width: 18px;
background-color: #fff; background-color: #fff;
border: 2px solid #d1d5db; /* gray-300 */ border: 2px solid #d1d5db;
border-radius: 4px; border-radius: 4px;
transition: all 0.2s ease; transition: all 0.2s ease;
display: inline-block; display: inline-block;
@@ -48,11 +47,11 @@ nav ul li a {
} }
.custom-checkbox:hover input ~ .checkmark { .custom-checkbox:hover input ~ .checkmark {
border-color: #6b7280; /* gray-500 */ border-color: #6b7280;
} }
.custom-checkbox input:checked ~ .checkmark { .custom-checkbox input:checked ~ .checkmark {
background-color: #6b7280; /* gray-500 */ background-color: #6b7280;
border-color: #6b7280; border-color: #6b7280;
} }
@@ -114,12 +113,29 @@ button:disabled {
} }
@keyframes shake { @keyframes shake {
0%, 100% { transform: translateX(0); } 0%,
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 100% {
20%, 40%, 60%, 80% { transform: translateX(5px); } transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-5px);
}
20%,
40%,
60%,
80% {
transform: translateX(5px);
}
} }
#req-length, #req-upper, #req-lower, #req-digit { #req-length,
#req-upper,
#req-lower,
#req-digit {
transition: color 0.2s ease; transition: color 0.2s ease;
} }
@@ -145,7 +161,8 @@ button:disabled {
} }
} }
#login-tab, #register-tab { #login-tab,
#register-tab {
font-family: "Dited", sans-serif; font-family: "Dited", sans-serif;
letter-spacing: 1.5px; letter-spacing: 1.5px;
cursor: pointer; cursor: pointer;
@@ -156,10 +173,73 @@ button:disabled {
} }
@keyframes dropdownFade { @keyframes dropdownFade {
from { opacity: 0; transform: translateY(-5px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
#user-arrow.rotate-180 { #user-arrow.rotate-180 {
transform: rotate(180deg); transform: rotate(180deg);
} }
.stat-number {
font-variant-numeric: tabular-nums;
}
.stat-card {
min-width: 140px;
}
@keyframes pulse-soft {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.animate-pulse-soft {
animation: pulse-soft 2s ease-in-out infinite;
}
#bookSvg {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out forwards;
}
.stat-card:hover svg {
transform: scale(1.1);
transition: transform 0.3s ease;
}
.stat-card svg {
transition: transform 0.3s ease;
}
.gradient-text {
background: linear-gradient(135deg, #374151 0%, #6b7280 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
+190 -56
View File
@@ -1,20 +1,32 @@
<!-- templates/auth.html --> {% extends "base.html" %} {% block title %}LiB - Авторизация{% endblock %} {%
{% extends "base.html" %} block content %}
{% block title %}LiB - Авторизация{% endblock %}
{% block content %}
<div class="flex flex-1 items-center justify-center p-4"> <div class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md"> <div class="w-full max-w-md">
<div class="bg-white rounded-lg shadow-md overflow-hidden"> <div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="flex border-b border-gray-200"> <div class="flex border-b border-gray-200">
<button type="button" id="login-tab" class="flex-1 py-4 text-center font-medium transition duration-200 text-gray-700 bg-gray-50 border-b-2 border-gray-500">Вход</button> <button
<button type="button" id="register-tab" class="flex-1 py-4 text-center font-medium transition duration-200 text-gray-400 hover:text-gray-600">Регистрация</button> type="button"
id="login-tab"
class="flex-1 py-4 text-center font-medium transition duration-200 text-gray-700 bg-gray-50 border-b-2 border-gray-500"
>
Вход
</button>
<button
type="button"
id="register-tab"
class="flex-1 py-4 text-center font-medium transition duration-200 text-gray-400 hover:text-gray-600"
>
Регистрация
</button>
</div> </div>
<form id="login-form" class="p-6"> <form id="login-form" class="p-6">
<div class="mb-4"> <div class="mb-4">
<label for="login-username" class="block text-sm font-medium text-gray-700 mb-2">Имя пользователя</label> <label
for="login-username"
class="block text-sm font-medium text-gray-700 mb-2"
>Имя пользователя</label
>
<input <input
type="text" type="text"
id="login-username" id="login-username"
@@ -24,9 +36,12 @@
required required
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="login-password" class="block text-sm font-medium text-gray-700 mb-2">Пароль</label> <label
for="login-password"
class="block text-sm font-medium text-gray-700 mb-2"
>Пароль</label
>
<div class="relative"> <div class="relative">
<input <input
type="password" type="password"
@@ -41,37 +56,77 @@
class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
onclick="togglePassword(this)" onclick="togglePassword(this)"
> >
<svg class="eye-open w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> class="eye-open w-5 h-5"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg> </svg>
<svg class="eye-closed w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path> class="eye-closed w-5 h-5 hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg> </svg>
</button> </button>
</div> </div>
</div> </div>
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<label class="custom-checkbox flex items-center text-sm text-gray-600"> <label
class="custom-checkbox flex items-center text-sm text-gray-600"
>
<input type="checkbox" id="remember-me" /> <input type="checkbox" id="remember-me" />
<span class="checkmark"></span>Запомнить меня <span class="checkmark"></span>Запомнить меня
</label> </label>
<a href="#" class="text-sm text-gray-500 hover:text-gray-700 transition">Забыли пароль?</a> <a
href="#"
class="text-sm text-gray-500 hover:text-gray-700 transition"
>Забыли пароль?</a
>
</div> </div>
<div
<div id="login-error" class="hidden mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg text-sm"></div> id="login-error"
class="hidden mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg text-sm"
></div>
<button <button
type="submit" type="submit"
id="login-submit" id="login-submit"
class="w-full bg-gray-500 text-white py-3 px-4 rounded-lg hover:bg-gray-600 transition duration-200 font-medium" class="w-full bg-gray-500 text-white py-3 px-4 rounded-lg hover:bg-gray-600 transition duration-200 font-medium"
>Войти</button> >
Войти
</button>
</form> </form>
<form
<form id="register-form" class="p-6 hidden" onsubmit="return handleRegister(event)"> id="register-form"
class="p-6 hidden"
onsubmit="return handleRegister(event);"
>
<div class="mb-4"> <div class="mb-4">
<label for="register-username" class="block text-sm font-medium text-gray-700 mb-2">Имя пользователя</label> <label
for="register-username"
class="block text-sm font-medium text-gray-700 mb-2"
>Имя пользователя</label
>
<input <input
type="text" type="text"
id="register-username" id="register-username"
@@ -83,9 +138,12 @@
maxlength="50" maxlength="50"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="register-email" class="block text-sm font-medium text-gray-700 mb-2">Email</label> <label
for="register-email"
class="block text-sm font-medium text-gray-700 mb-2"
>Email</label
>
<input <input
type="email" type="email"
id="register-email" id="register-email"
@@ -95,9 +153,15 @@
required required
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="register-fullname" class="block text-sm font-medium text-gray-700 mb-2">Полное имя <span class="text-gray-400">(необязательно)</span></label> <label
for="register-fullname"
class="block text-sm font-medium text-gray-700 mb-2"
>Полное имя
<span class="text-gray-400"
>(необязательно)</span
></label
>
<input <input
type="text" type="text"
id="register-fullname" id="register-fullname"
@@ -107,9 +171,12 @@
maxlength="100" maxlength="100"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="register-password" class="block text-sm font-medium text-gray-700 mb-2">Пароль</label> <label
for="register-password"
class="block text-sm font-medium text-gray-700 mb-2"
>Пароль</label
>
<div class="relative"> <div class="relative">
<input <input
type="password" type="password"
@@ -126,25 +193,61 @@
class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
onclick="togglePassword(this)" onclick="togglePassword(this)"
> >
<svg class="eye-open w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> class="eye-open w-5 h-5"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg> </svg>
<svg class="eye-closed w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path> class="eye-closed w-5 h-5 hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg> </svg>
</button> </button>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<div class="h-1 w-full bg-gray-200 rounded-full overflow-hidden"> <div
<div id="password-strength-bar" class="h-full w-0 transition-all duration-300"></div> class="h-1 w-full bg-gray-200 rounded-full overflow-hidden"
>
<div
id="password-strength-bar"
class="h-full w-0 transition-all duration-300"
></div>
</div> </div>
<p id="password-strength-text" class="text-xs mt-1 text-gray-500"></p> <p
id="password-strength-text"
class="text-xs mt-1 text-gray-500"
></p>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="register-password-confirm" class="block text-sm font-medium text-gray-700 mb-2">Подтвердите пароль</label> <label
for="register-password-confirm"
class="block text-sm font-medium text-gray-700 mb-2"
>Подтвердите пароль</label
>
<div class="relative"> <div class="relative">
<input <input
type="password" type="password"
@@ -159,22 +262,55 @@
class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
onclick="togglePassword(this)" onclick="togglePassword(this)"
> >
<svg class="eye-open w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> class="eye-open w-5 h-5"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg> </svg>
<svg class="eye-closed w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path> class="eye-closed w-5 h-5 hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg> </svg>
</button> </button>
</div> </div>
<p id="password-match-error" class="text-xs mt-1 text-red-500 hidden">Пароли не совпадают</p> <p
id="password-match-error"
class="text-xs mt-1 text-red-500 hidden"
>
Пароли не совпадают
</p>
</div> </div>
<div
<div id="register-error" class="hidden mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg text-sm"></div> id="register-error"
class="hidden mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg text-sm"
<div id="register-success" class="hidden mb-4 p-3 bg-green-100 border border-green-300 text-green-700 rounded-lg text-sm"></div> ></div>
<div
id="register-success"
class="hidden mb-4 p-3 bg-green-100 border border-green-300 text-green-700 rounded-lg text-sm"
></div>
<button <button
type="submit" type="submit"
id="register-submit" id="register-submit"
@@ -186,8 +322,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %} {% block scripts %}
{% block scripts %}
<script type="text/javascript" src="/static/auth.js"></script> <script type="text/javascript" src="/static/auth.js"></script>
{% endblock %} {% endblock %}
+1 -12
View File
@@ -5,7 +5,6 @@ content %}
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" 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> <h2 class="text-xl font-semibold mb-4">Поиск</h2>
<div class="relative mb-4"> <div class="relative mb-4">
<input <input
type="text" type="text"
@@ -29,9 +28,7 @@ content %}
/> />
</svg> </svg>
</div> </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">
@@ -52,12 +49,10 @@ content %}
></div> ></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" class="max-h-60 overflow-y-auto"></ul> <ul id="genres-list" class="max-h-60 overflow-y-auto"></ul>
</div> </div>
<button <button
id="apply-filters-btn" 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" class="w-full bg-gray-500 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition duration-200 mb-2"
@@ -70,19 +65,13 @@ content %}
> >
Сбросить фильтры Сбросить фильтры
</button> </button>
<!-- Счётчик результатов -->
<div <div
id="results-counter" id="results-counter"
class="mt-4 text-center text-sm text-gray-500" class="mt-4 text-center text-sm text-gray-500"
></div> ></div>
</aside> </aside>
<main class="flex-1"> <main class="flex-1">
<div id="books-container"> <div id="books-container"></div>
<!-- Книги будут загружены через JS -->
</div>
<!-- Пагинация добавляется динамически после books-container -->
</main> </main>
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
+195
View File
@@ -0,0 +1,195 @@
{% extends "base.html" %} {% block title %}LiB - Библиотека{% endblock %} {%
block content %}
<div class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-4xl">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="text-center py-8 border-b border-gray-200">
<h2 class="text-3xl font-bold text-gray-800 mb-2">
Добро пожаловать в LiB
</h2>
<p class="text-gray-500">Ваша персональная библиотека книг</p>
</div>
<div class="p-8">
<div
class="flex flex-col lg:flex-row items-center justify-center gap-12"
>
<div class="flex-shrink-0">
<svg
id="bookSvg"
width="400"
height="500"
viewBox="0 0 200 250"
class="drop-shadow-lg"
></svg>
</div>
<div class="grid grid-cols-2 gap-6">
<div
class="stat-card bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-6 text-center transform transition-all duration-300 hover:scale-105 hover:shadow-lg"
>
<div class="mb-3">
<svg
class="w-10 h-10 mx-auto text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
></path>
</svg>
</div>
<div
id="stat-books"
class="text-4xl font-bold text-gray-800 mb-1 stat-number"
data-target="0"
>
0
</div>
<div class="text-sm text-gray-500 font-medium">
Книг
</div>
</div>
<div
class="stat-card bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-6 text-center transform transition-all duration-300 hover:scale-105 hover:shadow-lg"
>
<div class="mb-3">
<svg
class="w-10 h-10 mx-auto text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
></path>
</svg>
</div>
<div
id="stat-authors"
class="text-4xl font-bold text-gray-800 mb-1 stat-number"
data-target="0"
>
0
</div>
<div class="text-sm text-gray-500 font-medium">
Авторов
</div>
</div>
<div
class="stat-card bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-6 text-center transform transition-all duration-300 hover:scale-105 hover:shadow-lg"
>
<div class="mb-3">
<svg
class="w-10 h-10 mx-auto text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
></path>
</svg>
</div>
<div
id="stat-genres"
class="text-4xl font-bold text-gray-800 mb-1 stat-number"
data-target="0"
>
0
</div>
<div class="text-sm text-gray-500 font-medium">
Жанров
</div>
</div>
<div
class="stat-card bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-6 text-center transform transition-all duration-300 hover:scale-105 hover:shadow-lg"
>
<div class="mb-3">
<svg
class="w-10 h-10 mx-auto text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
></path>
</svg>
</div>
<div
id="stat-users"
class="text-4xl font-bold text-gray-800 mb-1 stat-number"
data-target="0"
>
0
</div>
<div class="text-sm text-gray-500 font-medium">
Пользователей
</div>
</div>
</div>
</div>
</div>
<div class="px-8 pb-8">
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="/books"
class="inline-flex items-center justify-center px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition duration-200 font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
>
<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 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
></path>
</svg>
Смотреть книги
</a>
<a
href="/authors"
class="inline-flex items-center justify-center px-6 py-3 bg-white text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition duration-200 font-medium shadow-sm hover:shadow-md transform hover:-translate-y-0.5"
>
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
></path>
</svg>
Все авторы
</a>
</div>
</div>
</div>
<div class="mt-6 text-center text-gray-400 text-sm">
<p>LiB — Библиотека. Создано с ❤️</p>
</div>
</div>
</div>
{% endblock %} {% block scripts %}
<script type="text/javascript" src="/static/index.js"></script>
{% endblock %}