feat(frontend): add dynamic animations to login and admin panel
This commit is contained in:
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
name: git-commit-formatter
|
|
||||||
description: Форматирует сообщения коммитов git в соответствии со спецификацией Conventional Commits. Используйте этот навык, когда пользователь просит закоммитить изменения или написать сообщение к коммиту.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Навык форматирования коммитов Git
|
|
||||||
|
|
||||||
При написании сообщения коммита вы ДОЛЖНЫ следовать спецификации Conventional Commits.
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
`<type>[optional scope]: <description>`
|
|
||||||
|
|
||||||
## Допустимые типы
|
|
||||||
- **feat**: Новая функциональность
|
|
||||||
- **fix**: Исправление ошибки
|
|
||||||
- **docs**: Изменения только в документации
|
|
||||||
- **style**: Изменения, не влияющие на смысл кода (пробелы, форматирование и т.д.)
|
|
||||||
- **refactor**: Изменение кода, которое не исправляет ошибку и не добавляет функциональность
|
|
||||||
- **perf**: Изменение кода, повышающее производительность
|
|
||||||
- **test**: Добавление недостающих тестов или исправление существующих
|
|
||||||
- **chore**: Изменения в процессе сборки или вспомогательных инструментах и библиотеках
|
|
||||||
|
|
||||||
## Инструкции
|
|
||||||
1. Проанализируйте изменения, чтобы определить основной тип (`type`).
|
|
||||||
2. Определите область (`scope`), если это применимо (например, конкретный компонент или файл).
|
|
||||||
3. Напишите краткое описание (`description`) в повелительном наклонении (например, "add feature", а не "added feature").
|
|
||||||
4. Если есть критические изменения, добавьте подвал, начинающийся с `BREAKING CHANGE:`.
|
|
||||||
|
|
||||||
## Пример
|
|
||||||
`feat(auth): implement login with google`
|
|
||||||
@@ -17,7 +17,7 @@ if git diff-index --quiet HEAD --; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Выполняем цепочку команд локально (SSH URL: ssh://git@gitea.zuev.company:2222/Zuev/magistr.git)
|
# Выполняем цепочку команд локально (SSH URL: ssh://git@192.168.1.87:2222/Zuev/magistr.git)
|
||||||
git add . && \
|
git add . && \
|
||||||
git commit -m "$COMMIT_MSG" && \
|
git commit -m "$COMMIT_MSG" && \
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ body {
|
|||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
color: var(--text-primary);
|
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 {
|
.nav-item.active {
|
||||||
@@ -199,12 +208,38 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Cards ===== */
|
/* ===== Cards ===== */
|
||||||
|
@keyframes slideUpCard {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--bg-card-border);
|
border: 1px solid var(--bg-card-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
transition: background 0.4s ease, border-color 0.4s ease;
|
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 {
|
.card h2 {
|
||||||
@@ -298,6 +333,18 @@ body {
|
|||||||
box-shadow: 0 4px 16px var(--accent-glow);
|
box-shadow: 0 4px 16px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slideDownAlert {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-alert {
|
.form-alert {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 0.6rem 1rem;
|
padding: 0.6rem 1rem;
|
||||||
@@ -311,6 +358,7 @@ body {
|
|||||||
background: rgba(248, 113, 113, 0.1);
|
background: rgba(248, 113, 113, 0.1);
|
||||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
|
animation: slideDownAlert 0.3s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-alert.success {
|
.form-alert.success {
|
||||||
@@ -318,6 +366,7 @@ body {
|
|||||||
background: rgba(52, 211, 153, 0.1);
|
background: rgba(52, 211, 153, 0.1);
|
||||||
border: 1px solid rgba(52, 211, 153, 0.2);
|
border: 1px solid rgba(52, 211, 153, 0.2);
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
|
animation: slideDownAlert 0.3s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Table ===== */
|
/* ===== Table ===== */
|
||||||
@@ -347,8 +396,45 @@ tbody td {
|
|||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
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 {
|
tbody tr {
|
||||||
transition: background var(--transition);
|
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 {
|
tbody tr:hover {
|
||||||
@@ -477,11 +563,29 @@ tbody tr:hover {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background var(--transition);
|
transition: background var(--transition), transform var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete:hover {
|
.btn-delete:hover {
|
||||||
background: rgba(248, 113, 113, 0.2);
|
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 ===== */
|
/* ===== Mobile Menu Toggle ===== */
|
||||||
|
|||||||
@@ -16,6 +16,30 @@
|
|||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
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
|
// Users
|
||||||
const usersTbody = document.getElementById('users-tbody');
|
const usersTbody = document.getElementById('users-tbody');
|
||||||
const createForm = document.getElementById('create-form');
|
const createForm = document.getElementById('create-form');
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="login-form" novalidate>
|
<form id="login-form" novalidate>
|
||||||
<div class="form-group">
|
<div class="form-group stagger-1">
|
||||||
<label for="username">Имя пользователя</label>
|
<label for="username">Имя пользователя</label>
|
||||||
<div class="input-wrapper">
|
<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">
|
<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>
|
<span class="error-message" id="username-error"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group stagger-2">
|
||||||
<label for="password">Пароль</label>
|
<label for="password">Пароль</label>
|
||||||
<div class="input-wrapper">
|
<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">
|
<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>
|
<span class="error-message" id="password-error"></span>
|
||||||
</div>
|
</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-text">Войти</span>
|
||||||
<span class="btn-loader" hidden>
|
<span class="btn-loader" hidden>
|
||||||
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24">
|
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -12,6 +12,24 @@
|
|||||||
const btnLoader = btnSubmit.querySelector('.btn-loader');
|
const btnLoader = btnSubmit.querySelector('.btn-loader');
|
||||||
const togglePassword = document.getElementById('toggle-password');
|
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', () => {
|
togglePassword.addEventListener('click', () => {
|
||||||
const isPassword = passwordInput.type === 'password';
|
const isPassword = passwordInput.type === 'password';
|
||||||
passwordInput.type = isPassword ? 'text' : 'password';
|
passwordInput.type = isPassword ? 'text' : 'password';
|
||||||
@@ -27,8 +45,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setFieldError(input, errorEl, message) {
|
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;
|
errorEl.textContent = message;
|
||||||
|
|
||||||
|
// Shake animation
|
||||||
|
group.classList.remove('shake');
|
||||||
|
void group.offsetWidth; // trigger reflow
|
||||||
|
group.classList.add('shake');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAlert(message, type) {
|
function showAlert(message, type) {
|
||||||
|
|||||||
@@ -46,8 +46,21 @@
|
|||||||
transition: background 0.4s ease, color 0.4s ease;
|
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 {
|
.placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
animation: fadeInScale 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder h1 {
|
.placeholder h1 {
|
||||||
|
|||||||
@@ -303,6 +303,28 @@ body {
|
|||||||
transition: opacity var(--transition);
|
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 ===== */
|
||||||
.form-alert {
|
.form-alert {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -318,6 +340,7 @@ body {
|
|||||||
background: rgba(248, 113, 113, 0.1);
|
background: rgba(248, 113, 113, 0.1);
|
||||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
|
animation: slideDownAlert 0.3s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-alert.success {
|
.form-alert.success {
|
||||||
@@ -325,10 +348,13 @@ body {
|
|||||||
background: rgba(52, 211, 153, 0.1);
|
background: rgba(52, 211, 153, 0.1);
|
||||||
border: 1px solid rgba(52, 211, 153, 0.2);
|
border: 1px solid rgba(52, 211, 153, 0.2);
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
|
animation: slideDownAlert 0.3s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Submit Button ===== */
|
/* ===== Submit Button ===== */
|
||||||
.btn-submit {
|
.btn-submit {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -365,6 +391,23 @@ body {
|
|||||||
transform: none;
|
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 ===== */
|
||||||
.spinner {
|
.spinner {
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
|
|||||||
@@ -46,8 +46,21 @@
|
|||||||
transition: background 0.4s ease, color 0.4s ease;
|
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 {
|
.placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
animation: fadeInScale 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder h1 {
|
.placeholder h1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user