feat: login/register mode toggle with wave background animation
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- Login Screen -->
|
<!-- Login Screen -->
|
||||||
<div id="login-screen" class="screen">
|
<div id="login-screen" class="screen">
|
||||||
<div class="login-container">
|
<div class="login-container" id="login-container">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<div class="brand-icon">
|
<div class="brand-icon">
|
||||||
<svg width="40" height="40" viewBox="0 0 56 56" fill="none">
|
<svg width="40" height="40" viewBox="0 0 56 56" fill="none">
|
||||||
@@ -26,8 +26,9 @@
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="login-title" data-i18n="login.title">Sign In</h1>
|
<h1 class="login-title" id="login-title" data-i18n="login.title">Sign In</h1>
|
||||||
<p class="login-subtitle" data-i18n="login.subtitle">Welcome back to ZernMC</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<form id="login-form" class="login-form">
|
<form id="login-form" class="login-form">
|
||||||
@@ -39,14 +40,18 @@
|
|||||||
<label for="password" data-i18n="login.password">Password</label>
|
<label for="password" data-i18n="login.password">Password</label>
|
||||||
<input type="password" id="password" autocomplete="current-password" required>
|
<input type="password" id="password" autocomplete="current-password" required>
|
||||||
</div>
|
</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>
|
<p id="login-error" class="error-msg hidden"></p>
|
||||||
<button type="submit" class="btn-primary" id="login-btn">
|
<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>
|
<div class="spinner hidden"></div>
|
||||||
</button>
|
</button>
|
||||||
<div class="login-footer">
|
<div class="login-footer" id="login-footer">
|
||||||
<span data-i18n="login.hint">Need an account?</span>
|
<span id="footer-hint" data-i18n="login.hint">Need an account?</span>
|
||||||
<span class="login-footer-action" data-i18n="login.autoHint">It's free — just sign in</span>
|
<span class="login-footer-action" id="footer-action" data-i18n="login.register">Register</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ const LOCALES = {
|
|||||||
'login.title': 'Sign In', 'login.username': 'Username', 'login.password': 'Password',
|
'login.title': 'Sign In', 'login.username': 'Username', 'login.password': 'Password',
|
||||||
'login.subtitle': 'Welcome back to ZernMC',
|
'login.subtitle': 'Welcome back to ZernMC',
|
||||||
'login.hint': 'Need an account?',
|
'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...',
|
'login.signingIn': 'Signing in...',
|
||||||
'loading.text': 'Loading...',
|
'loading.text': 'Loading...',
|
||||||
'sidebar.serverPacks': 'Server Packs', 'sidebar.localPacks': 'Local Packs',
|
'sidebar.serverPacks': 'Server Packs', 'sidebar.localPacks': 'Local Packs',
|
||||||
@@ -184,7 +190,13 @@ const LOCALES = {
|
|||||||
'login.title': 'Вход', 'login.username': 'Логин', 'login.password': 'Пароль',
|
'login.title': 'Вход', 'login.username': 'Логин', 'login.password': 'Пароль',
|
||||||
'login.subtitle': 'С возвращением в ZernMC',
|
'login.subtitle': 'С возвращением в ZernMC',
|
||||||
'login.hint': 'Нет аккаунта?',
|
'login.hint': 'Нет аккаунта?',
|
||||||
'login.autoHint': 'Это бесплатно \u2014 просто войдите',
|
'login.register': 'Регистрация',
|
||||||
|
'login.registerTitle': 'Создать аккаунт',
|
||||||
|
'login.registerSubtitle': 'Присоединяйтесь к ZernMC',
|
||||||
|
'login.hasAccount': 'Уже есть аккаунт?',
|
||||||
|
'login.confirm': 'Подтвердите пароль',
|
||||||
|
'login.passMismatch': 'Пароли не совпадают',
|
||||||
|
'login.passTooShort': 'Пароль должен быть минимум 3 символа',
|
||||||
'login.signingIn': 'Вход...',
|
'login.signingIn': 'Вход...',
|
||||||
'loading.text': 'Загрузка...',
|
'loading.text': 'Загрузка...',
|
||||||
'sidebar.serverPacks': 'Серверные сборки', 'sidebar.localPacks': 'Локальные сборки',
|
'sidebar.serverPacks': 'Серверные сборки', 'sidebar.localPacks': 'Локальные сборки',
|
||||||
@@ -419,28 +431,43 @@ class ZernMCLauncher {
|
|||||||
initBg() {
|
initBg() {
|
||||||
const c = document.getElementById('bg-canvas');
|
const c = document.getElementById('bg-canvas');
|
||||||
const ctx = c.getContext('2d');
|
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 = () => {
|
const draw = () => {
|
||||||
ctx.clearRect(0, 0, c.width, c.height);
|
ctx.clearRect(0, 0, c.width, c.height);
|
||||||
const gs = 48, r = 1.2;
|
const w = c.width, h = c.height;
|
||||||
ctx.fillStyle = '#e94560';
|
modePulse += (0 - modePulse) * 0.03;
|
||||||
for (let x = 0; x <= c.width; x += gs)
|
|
||||||
for (let y = 0; y <= c.height; y += gs)
|
const baseAlpha = 0.04 + modePulse * 0.03;
|
||||||
ctx.beginPath(), ctx.arc(x + ox * 8, y + oy * 8, r, 0, Math.PI * 2), ctx.fill();
|
const amp = 20 + modePulse * 10;
|
||||||
};
|
const freq = 0.008 + modePulse * 0.003;
|
||||||
window.addEventListener('resize', resize);
|
const speed = 0.008;
|
||||||
window.addEventListener('mousemove', e => {
|
const lines = 5;
|
||||||
mx = (e.clientX / innerWidth - 0.5) * 2;
|
|
||||||
my = (e.clientY / innerHeight - 0.5) * 2;
|
for (let i = 0; i < lines; i++) {
|
||||||
});
|
const yOff = (h / (lines + 1)) * (i + 1);
|
||||||
const anim = () => {
|
const alpha = baseAlpha * (1 - i * 0.12);
|
||||||
ox += (mx * 0.3 - ox) * 0.04;
|
ctx.beginPath();
|
||||||
oy += (my * 0.3 - oy) * 0.04;
|
for (let x = 0; x <= w; x += 2) {
|
||||||
draw();
|
const wave = Math.sin(x * freq + t * speed + i * 1.8) * amp;
|
||||||
requestAnimationFrame(anim);
|
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();
|
resize();
|
||||||
anim();
|
anim();
|
||||||
}
|
}
|
||||||
@@ -513,30 +540,37 @@ class ZernMCLauncher {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const username = document.getElementById('username').value.trim();
|
const username = document.getElementById('username').value.trim();
|
||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
console.log('handleLogin: attempting login for', username);
|
|
||||||
const errEl = document.getElementById('login-error');
|
const errEl = document.getElementById('login-error');
|
||||||
const btn = document.getElementById('login-btn');
|
const btn = document.getElementById('login-btn');
|
||||||
const label = btn.querySelector('.btn-label');
|
const label = document.getElementById('login-btn-label');
|
||||||
const spinner = btn.querySelector('.spinner');
|
const spinner = btn.querySelector('.spinner');
|
||||||
|
|
||||||
if (!username || !password) { this.showLoginError(t('toast.enterCredentials')); return; }
|
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;
|
btn.disabled = true;
|
||||||
label.textContent = t('login.signingIn');
|
label.textContent = t('login.signingIn');
|
||||||
spinner.classList.remove('hidden');
|
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;
|
btn.disabled = false;
|
||||||
label.textContent = t('login.title');
|
label.textContent = this._registerMode ? t('login.register') : t('login.title');
|
||||||
spinner.classList.add('hidden');
|
spinner.classList.add('hidden');
|
||||||
|
|
||||||
if (r.success) {
|
if (r.success) {
|
||||||
this.state.account = r.data;
|
this.state.account = r.data;
|
||||||
this.enterMain();
|
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 {
|
} 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 }) });
|
var reg = await this.req('/register', { method: 'POST', body: JSON.stringify({ username, password }) });
|
||||||
if (reg.success) {
|
if (reg.success) {
|
||||||
this.state.account = reg.data;
|
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) {
|
showLoginError(msg) {
|
||||||
const el = document.getElementById('login-error');
|
const el = document.getElementById('login-error');
|
||||||
el.textContent = msg;
|
el.textContent = msg;
|
||||||
@@ -604,6 +677,7 @@ class ZernMCLauncher {
|
|||||||
// ==================== NAV ====================
|
// ==================== NAV ====================
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
document.getElementById('login-form').addEventListener('submit', e => this.handleLogin(e));
|
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('logout-btn').addEventListener('click', () => this.logout());
|
||||||
document.getElementById('settings-btn').addEventListener('click', () => this.switchView('settings'));
|
document.getElementById('settings-btn').addEventListener('click', () => this.switchView('settings'));
|
||||||
|
|
||||||
|
|||||||
@@ -59,21 +59,26 @@ body {
|
|||||||
|
|
||||||
/* ========== LOGIN ========== */
|
/* ========== LOGIN ========== */
|
||||||
.login-container {
|
.login-container {
|
||||||
|
position: relative; z-index: 1;
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: 32px 32px 24px;
|
padding: 32px 32px 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
box-shadow: 0 8px 40px rgba(0,0,0,0.5);
|
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; }
|
.brand-icon { margin-bottom: 12px; }
|
||||||
.login-title {
|
.login-title {
|
||||||
font-size: 22px; font-weight: 700;
|
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; }
|
.login-form { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
|
||||||
@@ -101,6 +106,11 @@ body {
|
|||||||
font-family: var(--font); cursor: pointer; outline: none;
|
font-family: var(--font); cursor: pointer; outline: none;
|
||||||
}
|
}
|
||||||
.field select:focus { border-color: var(--accent); }
|
.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 {
|
.btn-primary {
|
||||||
width: 100%; padding: 14px; border: none; border-radius: 4px;
|
width: 100%; padding: 14px; border: none; border-radius: 4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user