mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Добавление главной страницы о общей статистикой
This commit is contained in:
@@ -33,7 +33,7 @@ def get_info(app) -> Dict:
|
||||
@router.get("/", include_in_schema=False)
|
||||
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)
|
||||
@@ -85,7 +85,7 @@ async def api_info(app=Depends(lambda: get_app())):
|
||||
description="Возвращает статистическую информацию о системе",
|
||||
)
|
||||
async def api_stats(session: Session = Depends(get_session)):
|
||||
"""Эндпоинт стстистика системы"""
|
||||
"""Эндпоинт стстистики системы"""
|
||||
authors = select(func.count()).select_from(Author)
|
||||
books = select(func.count()).select_from(Book)
|
||||
genres = select(func.count()).select_from(Genre)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<?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"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -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();
|
||||
});
|
||||
@@ -19,7 +19,6 @@ nav ul li a {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
/* Custom checkbox styles */
|
||||
.custom-checkbox {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -40,7 +39,7 @@ nav ul li a {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #d1d5db; /* gray-300 */
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-block;
|
||||
@@ -48,11 +47,11 @@ nav ul li a {
|
||||
}
|
||||
|
||||
.custom-checkbox:hover input ~ .checkmark {
|
||||
border-color: #6b7280; /* gray-500 */
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.custom-checkbox input:checked ~ .checkmark {
|
||||
background-color: #6b7280; /* gray-500 */
|
||||
background-color: #6b7280;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
@@ -114,12 +113,29 @@ button:disabled {
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
0%,
|
||||
100% {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -145,7 +161,8 @@ button:disabled {
|
||||
}
|
||||
}
|
||||
|
||||
#login-tab, #register-tab {
|
||||
#login-tab,
|
||||
#register-tab {
|
||||
font-family: "Dited", sans-serif;
|
||||
letter-spacing: 1.5px;
|
||||
cursor: pointer;
|
||||
@@ -156,10 +173,73 @@ button:disabled {
|
||||
}
|
||||
|
||||
@keyframes dropdownFade {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#user-arrow.rotate-180 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
<!-- templates/auth.html -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}LiB - Авторизация{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% 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-md">
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
<form id="login-form" class="p-6">
|
||||
<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
|
||||
type="text"
|
||||
id="login-username"
|
||||
@@ -24,9 +36,12 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<input
|
||||
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"
|
||||
onclick="togglePassword(this)"
|
||||
>
|
||||
<svg class="eye-open w-5 h-5" 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
|
||||
class="eye-open w-5 h-5"
|
||||
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 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
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<span class="checkmark"></span>Запомнить меня
|
||||
</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 id="login-error" class="hidden mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg text-sm"></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>
|
||||
<button
|
||||
type="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"
|
||||
>Войти</button>
|
||||
>
|
||||
Войти
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form id="register-form" class="p-6 hidden" onsubmit="return handleRegister(event)">
|
||||
<form
|
||||
id="register-form"
|
||||
class="p-6 hidden"
|
||||
onsubmit="return handleRegister(event);"
|
||||
>
|
||||
<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
|
||||
type="text"
|
||||
id="register-username"
|
||||
@@ -83,9 +138,12 @@
|
||||
maxlength="50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
type="email"
|
||||
id="register-email"
|
||||
@@ -95,9 +153,15 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
type="text"
|
||||
id="register-fullname"
|
||||
@@ -107,9 +171,12 @@
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<input
|
||||
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"
|
||||
onclick="togglePassword(this)"
|
||||
>
|
||||
<svg class="eye-open w-5 h-5" 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
|
||||
class="eye-open w-5 h-5"
|
||||
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 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
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<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
|
||||
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>
|
||||
<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 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">
|
||||
<input
|
||||
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"
|
||||
onclick="togglePassword(this)"
|
||||
>
|
||||
<svg class="eye-open w-5 h-5" 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
|
||||
class="eye-open w-5 h-5"
|
||||
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 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
|
||||
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>
|
||||
</button>
|
||||
</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 id="register-error" class="hidden mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg text-sm"></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>
|
||||
|
||||
<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>
|
||||
<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
|
||||
type="submit"
|
||||
id="register-submit"
|
||||
@@ -186,8 +322,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %} {% block scripts %}
|
||||
<script type="text/javascript" src="/static/auth.js"></script>
|
||||
{% endblock %}
|
||||
@@ -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"
|
||||
>
|
||||
<h2 class="text-xl font-semibold mb-4">Поиск</h2>
|
||||
|
||||
<div class="relative mb-4">
|
||||
<input
|
||||
type="text"
|
||||
@@ -29,9 +28,7 @@ content %}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold mb-4">Фильтры</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="font-medium mb-2">Авторы</h3>
|
||||
<div class="relative">
|
||||
@@ -52,12 +49,10 @@ content %}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="font-medium mb-2">Жанры</h3>
|
||||
<ul id="genres-list" class="max-h-60 overflow-y-auto"></ul>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
@@ -70,19 +65,13 @@ content %}
|
||||
>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
|
||||
<!-- Счётчик результатов -->
|
||||
<div
|
||||
id="results-counter"
|
||||
class="mt-4 text-center text-sm text-gray-500"
|
||||
></div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1">
|
||||
<div id="books-container">
|
||||
<!-- Книги будут загружены через JS -->
|
||||
</div>
|
||||
<!-- Пагинация добавляется динамически после books-container -->
|
||||
<div id="books-container"></div>
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user