feat: login/register mode toggle with wave background animation

This commit is contained in:
SashegDev
2026-07-30 10:25:26 +00:00
parent f32ac1ef98
commit d9c527b8db
3 changed files with 125 additions and 36 deletions
+12 -7
View File
@@ -12,7 +12,7 @@
<div id="app">
<!-- Login Screen -->
<div id="login-screen" class="screen">
<div class="login-container">
<div class="login-container" id="login-container">
<div class="login-header">
<div class="brand-icon">
<svg width="40" height="40" viewBox="0 0 56 56" fill="none">
@@ -26,8 +26,9 @@
</defs>
</svg>
</div>
<h1 class="login-title" data-i18n="login.title">Sign In</h1>
<p class="login-subtitle" data-i18n="login.subtitle">Welcome back to ZernMC</p>
<h1 class="login-title" id="login-title" data-i18n="login.title">Sign In</h1>
<p class="login-subtitle" id="login-subtitle" data-i18n="login.subtitle">Welcome back to ZernMC</p>
<span id="version" class="hidden">1.0.12</span>
</div>
<form id="login-form" class="login-form">
@@ -39,14 +40,18 @@
<label for="password" data-i18n="login.password">Password</label>
<input type="password" id="password" autocomplete="current-password" required>
</div>
<div class="field hidden" id="confirm-field">
<label for="confirm-password" data-i18n="login.confirm">Confirm Password</label>
<input type="password" id="confirm-password" autocomplete="new-password">
</div>
<p id="login-error" class="error-msg hidden"></p>
<button type="submit" class="btn-primary" id="login-btn">
<span class="btn-label" data-i18n="login.title">Sign In</span>
<span class="btn-label" id="login-btn-label" data-i18n="login.title">Sign In</span>
<div class="spinner hidden"></div>
</button>
<div class="login-footer">
<span data-i18n="login.hint">Need an account?</span>
<span class="login-footer-action" data-i18n="login.autoHint">It's free — just sign in</span>
<div class="login-footer" id="login-footer">
<span id="footer-hint" data-i18n="login.hint">Need an account?</span>
<span class="login-footer-action" id="footer-action" data-i18n="login.register">Register</span>
</div>
</form>
</div>
+100 -26
View File
@@ -6,7 +6,13 @@ const LOCALES = {
'login.title': 'Sign In', 'login.username': 'Username', 'login.password': 'Password',
'login.subtitle': 'Welcome back to ZernMC',
'login.hint': 'Need an account?',
'login.autoHint': "It's free \u2014 just sign in",
'login.register': 'Register',
'login.registerTitle': 'Create Account',
'login.registerSubtitle': 'Join ZernMC today',
'login.hasAccount': 'Already have an account?',
'login.confirm': 'Confirm Password',
'login.passMismatch': 'Passwords do not match',
'login.passTooShort': 'Password must be at least 3 characters',
'login.signingIn': 'Signing in...',
'loading.text': 'Loading...',
'sidebar.serverPacks': 'Server Packs', 'sidebar.localPacks': 'Local Packs',
@@ -184,7 +190,13 @@ const LOCALES = {
'login.title': 'Вход', 'login.username': 'Логин', 'login.password': 'Пароль',
'login.subtitle': 'С возвращением в ZernMC',
'login.hint': 'Нет аккаунта?',
'login.autoHint': 'Это бесплатно \u2014 просто войдите',
'login.register': 'Регистрация',
'login.registerTitle': 'Создать аккаунт',
'login.registerSubtitle': 'Присоединяйтесь к ZernMC',
'login.hasAccount': 'Уже есть аккаунт?',
'login.confirm': 'Подтвердите пароль',
'login.passMismatch': 'Пароли не совпадают',
'login.passTooShort': 'Пароль должен быть минимум 3 символа',
'login.signingIn': 'Вход...',
'loading.text': 'Загрузка...',
'sidebar.serverPacks': 'Серверные сборки', 'sidebar.localPacks': 'Локальные сборки',
@@ -419,28 +431,43 @@ class ZernMCLauncher {
initBg() {
const c = document.getElementById('bg-canvas');
const ctx = c.getContext('2d');
let mx = 0, my = 0, ox = 0, oy = 0;
let t = 0, mode = 'login', modePulse = 0;
this.setWaveMode = m => { mode = m; modePulse = 1; };
const resize = () => { c.width = window.innerWidth; c.height = window.innerHeight; };
window.addEventListener('resize', resize);
const resize = () => { c.width = window.innerWidth; c.height = window.innerHeight; draw(); };
const draw = () => {
ctx.clearRect(0, 0, c.width, c.height);
const gs = 48, r = 1.2;
ctx.fillStyle = '#e94560';
for (let x = 0; x <= c.width; x += gs)
for (let y = 0; y <= c.height; y += gs)
ctx.beginPath(), ctx.arc(x + ox * 8, y + oy * 8, r, 0, Math.PI * 2), ctx.fill();
};
window.addEventListener('resize', resize);
window.addEventListener('mousemove', e => {
mx = (e.clientX / innerWidth - 0.5) * 2;
my = (e.clientY / innerHeight - 0.5) * 2;
});
const anim = () => {
ox += (mx * 0.3 - ox) * 0.04;
oy += (my * 0.3 - oy) * 0.04;
draw();
requestAnimationFrame(anim);
const w = c.width, h = c.height;
modePulse += (0 - modePulse) * 0.03;
const baseAlpha = 0.04 + modePulse * 0.03;
const amp = 20 + modePulse * 10;
const freq = 0.008 + modePulse * 0.003;
const speed = 0.008;
const lines = 5;
for (let i = 0; i < lines; i++) {
const yOff = (h / (lines + 1)) * (i + 1);
const alpha = baseAlpha * (1 - i * 0.12);
ctx.beginPath();
for (let x = 0; x <= w; x += 2) {
const wave = Math.sin(x * freq + t * speed + i * 1.8) * amp;
const wave2 = Math.sin(x * freq * 0.5 + t * speed * 0.7 + i * 2.5) * amp * 0.5;
const y = yOff + wave + wave2;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.strokeStyle = `rgba(233, 69, 96, ${alpha})`;
ctx.lineWidth = 1.2;
ctx.stroke();
}
t++;
};
const anim = () => { draw(); requestAnimationFrame(anim); };
resize();
anim();
}
@@ -513,30 +540,37 @@ class ZernMCLauncher {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
console.log('handleLogin: attempting login for', username);
const errEl = document.getElementById('login-error');
const btn = document.getElementById('login-btn');
const label = btn.querySelector('.btn-label');
const label = document.getElementById('login-btn-label');
const spinner = btn.querySelector('.spinner');
if (!username || !password) { this.showLoginError(t('toast.enterCredentials')); return; }
if (this._registerMode) {
const confirm = document.getElementById('confirm-password').value;
if (password !== confirm) { this.showLoginError(t('login.passMismatch')); return; }
if (password.length < 3) { this.showLoginError(t('login.passTooShort')); return; }
}
btn.disabled = true;
label.textContent = t('login.signingIn');
spinner.classList.remove('hidden');
const r = await this.req('/login', { method: 'POST', body: JSON.stringify({ username, password }) });
const endpoint = this._registerMode ? '/register' : '/login';
const r = await this.req(endpoint, { method: 'POST', body: JSON.stringify({ username, password }) });
btn.disabled = false;
label.textContent = t('login.title');
label.textContent = this._registerMode ? t('login.register') : t('login.title');
spinner.classList.add('hidden');
if (r.success) {
this.state.account = r.data;
this.enterMain();
this.toast(tr('toast.welcome', null, {username: r.data.username}), 'success');
const key = this._registerMode ? 'toast.accountCreated' : 'toast.welcome';
this.toast(tr(key, null, {username: r.data.username}), 'success');
} else {
if (r.error && (r.error.includes('not found') || r.error.includes('Invalid'))) {
if (!this._registerMode && r.error && (r.error.includes('not found') || r.error.includes('Invalid'))) {
var reg = await this.req('/register', { method: 'POST', body: JSON.stringify({ username, password }) });
if (reg.success) {
this.state.account = reg.data;
@@ -549,6 +583,45 @@ class ZernMCLauncher {
}
}
toggleMode() {
this._registerMode = !this._registerMode;
const isReg = this._registerMode;
const container = document.getElementById('login-container');
const title = document.getElementById('login-title');
const subtitle = document.getElementById('login-subtitle');
const btnLabel = document.getElementById('login-btn-label');
const footerHint = document.getElementById('footer-hint');
const footerAction = document.getElementById('footer-action');
const confirmField = document.getElementById('confirm-field');
container.style.transform = 'translateY(-4px)';
container.style.opacity = '0.6';
setTimeout(() => {
title.textContent = isReg ? t('login.registerTitle') : t('login.title');
subtitle.textContent = isReg ? t('login.registerSubtitle') : t('login.subtitle');
btnLabel.textContent = isReg ? t('login.register') : t('login.title');
footerHint.textContent = isReg ? t('login.hasAccount') : t('login.hint');
footerAction.textContent = isReg ? t('login.title') : t('login.register');
if (isReg) {
confirmField.classList.remove('hidden');
document.getElementById('confirm-password').required = true;
} else {
confirmField.classList.add('hidden');
document.getElementById('confirm-password').required = false;
document.getElementById('confirm-password').value = '';
}
document.getElementById('login-error').classList.add('hidden');
container.style.transform = '';
container.style.opacity = '';
if (this.setWaveMode) this.setWaveMode(isReg ? 'register' : 'login');
}, 150);
}
showLoginError(msg) {
const el = document.getElementById('login-error');
el.textContent = msg;
@@ -604,6 +677,7 @@ class ZernMCLauncher {
// ==================== NAV ====================
bindEvents() {
document.getElementById('login-form').addEventListener('submit', e => this.handleLogin(e));
document.getElementById('login-footer').addEventListener('click', () => this.toggleMode());
document.getElementById('logout-btn').addEventListener('click', () => this.logout());
document.getElementById('settings-btn').addEventListener('click', () => this.switchView('settings'));
+13 -3
View File
@@ -59,21 +59,26 @@ body {
/* ========== LOGIN ========== */
.login-container {
position: relative; z-index: 1;
background: var(--bg-elevated);
border-radius: var(--radius-md);
padding: 32px 32px 24px;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 40px rgba(0,0,0,0.5);
transition: transform 0.35s ease, opacity 0.35s ease;
}
.login-header { text-align: center; margin-bottom: 28px; }
.login-header { text-align: center; margin-bottom: 28px; overflow: hidden; }
.brand-icon { margin-bottom: 12px; }
.login-title {
font-size: 22px; font-weight: 700;
color: var(--text);
color: var(--text); transition: transform 0.3s ease, opacity 0.3s ease;
}
.login-subtitle {
color: var(--text-muted); font-size: 13px; margin-top: 4px;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.login-subtitle { color: var(--text-muted); font-size: 13px; margin-top: 4px; }
.login-form { display: flex; flex-direction: column; gap: 16px; }
@@ -101,6 +106,11 @@ body {
font-family: var(--font); cursor: pointer; outline: none;
}
.field select:focus { border-color: var(--accent); }
#confirm-field {
transition: opacity 0.3s ease, max-height 0.3s ease, margin 0.3s ease;
overflow: hidden; max-height: 60px; opacity: 1; margin: 0;
}
#confirm-field.hidden { max-height: 0; opacity: 0; margin: 0; display: block !important; padding: 0; overflow: hidden; pointer-events: none; }
.btn-primary {
width: 100%; padding: 14px; border: none; border-radius: 4px;