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
|
||||
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 commit -m "$COMMIT_MSG" && \
|
||||
git push origin main
|
||||
|
||||
@@ -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 ===== */
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user