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 @@ - - + + - \ No newline at end of file + fill-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" + fill="#000000" + /> + 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 %}
- - + +
- - Имя пользователя +
-
- +
- -
-
-
- - - -
+ + > + Войти + - -
-{% endblock %} - -{% block scripts %} +{% endblock %} {% block scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/library_service/templates/books.html b/library_service/templates/books.html index 42ffc5f..b9fc378 100644 --- a/library_service/templates/books.html +++ b/library_service/templates/books.html @@ -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" >

Поиск

-
-

Фильтры

-

Авторы

@@ -52,12 +49,10 @@ content %} >
-

Жанры

- - -
-
-
- -
- +
{% endblock %} {% block scripts %} diff --git a/library_service/templates/index.html b/library_service/templates/index.html new file mode 100644 index 0000000..16b18f6 --- /dev/null +++ b/library_service/templates/index.html @@ -0,0 +1,195 @@ +{% extends "base.html" %} {% block title %}LiB - Библиотека{% endblock %} {% +block content %} +
+
+
+
+

+ Добро пожаловать в LiB +

+

Ваша персональная библиотека книг

+
+
+
+
+ +
+
+
+
+ + + +
+
+ 0 +
+
+ Книг +
+
+
+
+ + + +
+
+ 0 +
+
+ Авторов +
+
+
+
+ + + +
+
+ 0 +
+
+ Жанров +
+
+
+
+ + + +
+
+ 0 +
+
+ Пользователей +
+
+
+
+
+ +
+
+

LiB — Библиотека. Создано с ❤️

+
+
+
+{% endblock %} {% block scripts %} + +{% endblock %}