diff --git a/library_service/routers/misc.py b/library_service/routers/misc.py index 0362408..1133ca7 100644 --- a/library_service/routers/misc.py +++ b/library_service/routers/misc.py @@ -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) diff --git a/library_service/static/avatar.svg b/library_service/static/avatar.svg index 4168b04..ef1bcfa 100644 --- a/library_service/static/avatar.svg +++ b/library_service/static/avatar.svg @@ -1,9 +1,15 @@ - - diff --git a/library_service/static/index.js b/library_service/static/index.js new file mode 100644 index 0000000..21523b9 --- /dev/null +++ b/library_service/static/index.js @@ -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(); +}); diff --git a/library_service/static/styles.css b/library_service/static/styles.css index cb44c55..c641c27 100644 --- a/library_service/static/styles.css +++ b/library_service/static/styles.css @@ -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); -} \ No newline at end of file +} + +.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; +} diff --git a/library_service/templates/auth.html b/library_service/templates/auth.html index 31e0c95..715ec25 100644 --- a/library_service/templates/auth.html +++ b/library_service/templates/auth.html @@ -1,80 +1,135 @@ - -{% extends "base.html" %} - -{% block title %}LiB - Авторизация{% endblock %} - -{% block content %} +{% extends "base.html" %} {% block title %}LiB - Авторизация{% endblock %} {% +block content %}
Ваша персональная библиотека книг
+LiB — Библиотека. Создано с ❤️
+