feat(frontend): add dynamic animations to login and admin panel

This commit is contained in:
Zuev
2026-02-20 00:48:03 +03:00
parent e9c08b4c75
commit 86a29f6419
9 changed files with 228 additions and 37 deletions

View File

@@ -134,6 +134,15 @@ body {
.nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
transform: translateX(4px);
}
.nav-item svg {
transition: transform var(--transition);
}
.nav-item:hover svg {
transform: scale(1.1);
}
.nav-item.active {
@@ -199,12 +208,38 @@ body {
}
/* ===== Cards ===== */
@keyframes slideUpCard {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
background: var(--bg-card);
border: 1px solid var(--bg-card-border);
border-radius: var(--radius-md);
padding: 1.5rem;
transition: background 0.4s ease, border-color 0.4s ease;
animation: slideUpCard 0.4s ease-out both;
}
/* Staggered cards */
.card:nth-child(1) {
animation-delay: 0.1s;
}
.card:nth-child(2) {
animation-delay: 0.2s;
}
.card:nth-child(3) {
animation-delay: 0.3s;
}
.card h2 {
@@ -298,6 +333,18 @@ body {
box-shadow: 0 4px 16px var(--accent-glow);
}
@keyframes slideDownAlert {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-alert {
display: none;
padding: 0.6rem 1rem;
@@ -311,6 +358,7 @@ body {
background: rgba(248, 113, 113, 0.1);
border: 1px solid rgba(248, 113, 113, 0.2);
color: var(--error);
animation: slideDownAlert 0.3s ease-out both;
}
.form-alert.success {
@@ -318,6 +366,7 @@ body {
background: rgba(52, 211, 153, 0.1);
border: 1px solid rgba(52, 211, 153, 0.2);
color: var(--success);
animation: slideDownAlert 0.3s ease-out both;
}
/* ===== Table ===== */
@@ -347,8 +396,45 @@ tbody td {
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
@keyframes slideInRow {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
tbody tr {
transition: background var(--transition);
animation: slideInRow 0.3s ease-out both;
}
tbody tr:nth-child(1) {
animation-delay: 0.05s;
}
tbody tr:nth-child(2) {
animation-delay: 0.1s;
}
tbody tr:nth-child(3) {
animation-delay: 0.15s;
}
tbody tr:nth-child(4) {
animation-delay: 0.2s;
}
tbody tr:nth-child(5) {
animation-delay: 0.25s;
}
tbody tr:nth-child(n+6) {
animation-delay: 0.3s;
}
tbody tr:hover {
@@ -477,11 +563,29 @@ tbody tr:hover {
font-family: inherit;
font-size: 0.8rem;
cursor: pointer;
transition: background var(--transition);
transition: background var(--transition), transform var(--transition);
}
.btn-delete:hover {
background: rgba(248, 113, 113, 0.2);
transform: scale(1.05);
}
/* ===== Ripple Effect ===== */
.ripple {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: admin-ripple 0.6s linear;
background-color: rgba(255, 255, 255, 0.3);
pointer-events: none;
}
@keyframes admin-ripple {
to {
transform: scale(4);
opacity: 0;
}
}
/* ===== Mobile Menu Toggle ===== */

View File

@@ -16,6 +16,30 @@
const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
// Global Ripple Effect
document.addEventListener('click', function (e) {
const btn = e.target.closest('.btn-create, .btn-delete, .btn-logout');
if (!btn) return;
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const ripple = document.createElement('span');
ripple.classList.add('ripple');
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
if (getComputedStyle(btn).position === 'static') {
btn.style.position = 'relative';
}
btn.style.overflow = 'hidden';
btn.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
});
// Users
const usersTbody = document.getElementById('users-tbody');
const createForm = document.getElementById('create-form');

View File

@@ -37,7 +37,7 @@
</div>
<form id="login-form" novalidate>
<div class="form-group">
<div class="form-group stagger-1">
<label for="username">Имя пользователя</label>
<div class="input-wrapper">
<svg class="input-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -49,7 +49,7 @@
<span class="error-message" id="username-error"></span>
</div>
<div class="form-group">
<div class="form-group stagger-2">
<label for="password">Пароль</label>
<div class="input-wrapper">
<svg class="input-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -67,9 +67,9 @@
<span class="error-message" id="password-error"></span>
</div>
<div class="form-alert" id="form-alert" role="alert"></div>
<div class="form-alert stagger-3" id="form-alert" role="alert"></div>
<button type="submit" class="btn-submit" id="btn-submit">
<button type="submit" class="btn-submit stagger-4" id="btn-submit">
<span class="btn-text">Войти</span>
<span class="btn-loader" hidden>
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24">

View File

@@ -12,6 +12,24 @@
const btnLoader = btnSubmit.querySelector('.btn-loader');
const togglePassword = document.getElementById('toggle-password');
// Ripple effect
btnSubmit.addEventListener('click', function(e) {
const rect = this.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const ripple = document.createElement('span');
ripple.classList.add('ripple');
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
this.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
togglePassword.addEventListener('click', () => {
const isPassword = passwordInput.type === 'password';
passwordInput.type = isPassword ? 'text' : 'password';
@@ -27,8 +45,14 @@
}
function setFieldError(input, errorEl, message) {
input.closest('.form-group').classList.add('has-error');
const group = input.closest('.form-group');
group.classList.add('has-error');
errorEl.textContent = message;
// Shake animation
group.classList.remove('shake');
void group.offsetWidth; // trigger reflow
group.classList.add('shake');
}
function showAlert(message, type) {

View File

@@ -46,8 +46,21 @@
transition: background 0.4s ease, color 0.4s ease;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.placeholder {
text-align: center;
animation: fadeInScale 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.placeholder h1 {

View File

@@ -303,6 +303,28 @@ body {
transition: opacity var(--transition);
}
/* ===== Animations ===== */
.stagger-1 { animation: fadeInUp 0.5s ease-out 0.1s both; }
.stagger-2 { animation: fadeInUp 0.5s ease-out 0.2s both; }
.stagger-3 { animation: fadeInUp 0.5s ease-out 0.3s both; }
.stagger-4 { animation: fadeInUp 0.5s ease-out 0.4s both; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-6px); }
50% { transform: translateX(6px); }
75% { transform: translateX(-6px); }
}
.shake {
animation: shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes slideDownAlert {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===== Form Alert ===== */
.form-alert {
display: none;
@@ -318,6 +340,7 @@ body {
background: rgba(248, 113, 113, 0.1);
border: 1px solid rgba(248, 113, 113, 0.2);
color: var(--error);
animation: slideDownAlert 0.3s ease-out both;
}
.form-alert.success {
@@ -325,10 +348,13 @@ body {
background: rgba(52, 211, 153, 0.1);
border: 1px solid rgba(52, 211, 153, 0.2);
color: var(--success);
animation: slideDownAlert 0.3s ease-out both;
}
/* ===== Submit Button ===== */
.btn-submit {
position: relative;
overflow: hidden;
width: 100%;
padding: 0.85rem;
border: none;
@@ -365,6 +391,23 @@ body {
transform: none;
}
/* ===== Ripple Effect ===== */
.ripple {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: ripple 0.6s linear;
background-color: rgba(255, 255, 255, 0.3);
pointer-events: none;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
/* ===== Spinner ===== */
.spinner {
animation: spin 0.8s linear infinite;

View File

@@ -46,8 +46,21 @@
transition: background 0.4s ease, color 0.4s ease;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.placeholder {
text-align: center;
animation: fadeInScale 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.placeholder h1 {