UX: Auth как в Grok — табы Вход/Регистрация, живая валидация, toast
НОВЫЙ UX (от Grok):
- Один экран — табы Вход / Регистрация (мгновенное переключение)
- Все поля видны, кнопка всегда кликабельна
- Валидация в реальном времени: под каждым полем ✓/✗ с пояснением
- Состояния кнопки: idle / load ('⏳ Создаём...') / ok ('✓ Готово!') / err ('⚠️ ...')
- Toast 'Добро пожаловать, [имя]!' после успеха
- При ошибке: подсветка проблемного поля + фокус на нём
- Email живая валидация (regex), пароль ≥6 (как требует Grok / API: ≥8 → я ставлю 8 для совместимости с бэком)
- Пароль 2 — только в режиме Регистрация
- Переключение режима сбрасывает валидацию, фокус на первое пустое поле
CSS новые классы: .btn.ok, .btn.load, .btn.err, input.bad, input.good, .hint.ok/err/mute
Технически:
- showEmailLogin() рисует сразу табы + форму
- _initAuthForm() биндит всё
- _submitAuth(mode) делает реальный запрос
- showEmailRegister() → переключает таб на 'Регистрация'
This commit is contained in:
parent
a2cc623756
commit
9188088276
@ -500,3 +500,15 @@ label { font-size: 13px; color: var(--text-dim); margin-bottom: 4px; display: bl
|
||||
|
||||
/* edit marker */
|
||||
.bubble .edited { font-style: italic; opacity: 0.7; }
|
||||
|
||||
/* Auth UX (от Grok) */
|
||||
.btn.ok { background: var(--success) !important; color: #fff !important; }
|
||||
.btn.load { opacity: .85; pointer-events: none; }
|
||||
.btn.err { background: var(--danger) !important; }
|
||||
|
||||
input.bad { border-color: var(--danger) !important; }
|
||||
input.good { border-color: var(--success) !important; }
|
||||
|
||||
.hint.ok { color: var(--success); }
|
||||
.hint.err { color: var(--danger); }
|
||||
.hint.mute { color: var(--text-dim); }
|
||||
|
||||
408
public/js/app.js
408
public/js/app.js
@ -205,182 +205,56 @@ const App = {
|
||||
showEmailLogin() {
|
||||
document.getElementById('app').innerHTML = `
|
||||
<div class="view">
|
||||
<h1>Вход</h1>
|
||||
<div class="sub">Войди или зарегистрируйся по email</div>
|
||||
<div class="wrap" style="padding:0;">
|
||||
<div class="logo" style="text-align:center;margin:12px 0 18px;">
|
||||
<b style="font-size:26px;color:var(--primary);">🍻 BuhApp</b>
|
||||
<p style="color:var(--text-dim);font-size:13px;margin-top:2px;">Учёт · просто и быстро</p>
|
||||
</div>
|
||||
<div class="tabs" style="display:flex;background:var(--bg-elev);border-radius:12px;padding:4px;margin-bottom:18px;border:1px solid var(--border);">
|
||||
<button class="tab on" id="tIn" type="button" style="flex:1;border:0;background:var(--primary);color:#fff;font:600 14px/1 inherit;padding:12px;border-radius:9px;cursor:pointer;">Вход</button>
|
||||
<button class="tab" id="tUp" type="button" style="flex:1;border:0;background:0;color:var(--text-dim);font:600 14px/1 inherit;padding:12px;border-radius:9px;cursor:pointer;">Регистрация</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<label>Email</label>
|
||||
<input class="input" id="el-email" type="email" placeholder="you@example.com" autocomplete="email">
|
||||
<label>Пароль (≥8 символов)</label>
|
||||
<input class="input" id="el-pass" type="password" placeholder="минимум 8 символов" autocomplete="current-password">
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:14px;">
|
||||
<input type="checkbox" id="el-remember" checked>
|
||||
<span>Запомнить меня (30 дней)</span>
|
||||
<form id="b-auth-form" novalidate>
|
||||
<div id="regBox" style="display:none;">
|
||||
<div class="field" style="margin-bottom:14px;">
|
||||
<label for="b-name" style="display:block;font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 6px;">Имя</label>
|
||||
<input id="b-name" type="text" autocomplete="name" placeholder="Как к вам обращаться" style="width:100%;background:var(--bg-elev);border:1.5px solid var(--border);border-radius:10px;color:var(--text);font:15px/1.2 inherit;padding:13px 14px;outline:0;-webkit-appearance:none;">
|
||||
<div class="hint mute" id="hName" style="min-height:18px;font-size:12px;margin-top:5px;color:var(--text-dim);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:14px;">
|
||||
<label for="b-email" style="display:block;font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 6px;">Email</label>
|
||||
<input id="b-email" type="email" autocomplete="email" inputmode="email" placeholder="you@mail.com" style="width:100%;background:var(--bg-elev);border:1.5px solid var(--border);border-radius:10px;color:var(--text);font:15px/1.2 inherit;padding:13px 14px;outline:0;-webkit-appearance:none;">
|
||||
<div class="hint mute" id="hEmail" style="min-height:18px;font-size:12px;margin-top:5px;color:var(--text-dim);"></div>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:14px;">
|
||||
<label for="b-pass" style="display:block;font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 6px;">Пароль</label>
|
||||
<input id="b-pass" type="password" autocomplete="current-password" placeholder="Минимум 6 символов" style="width:100%;background:var(--bg-elev);border:1.5px solid var(--border);border-radius:10px;color:var(--text);font:15px/1.2 inherit;padding:13px 14px;outline:0;-webkit-appearance:none;">
|
||||
<div class="hint mute" id="hPass" style="min-height:18px;font-size:12px;margin-top:5px;color:var(--text-dim);"></div>
|
||||
</div>
|
||||
<div id="regBox2" style="display:none;">
|
||||
<div class="field" style="margin-bottom:14px;">
|
||||
<label for="b-pass2" style="display:block;font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 6px;">Ещё раз пароль</label>
|
||||
<input id="b-pass2" type="password" autocomplete="new-password" placeholder="Повторите пароль" style="width:100%;background:var(--bg-elev);border:1.5px solid var(--border);border-radius:10px;color:var(--text);font:15px/1.2 inherit;padding:13px 14px;outline:0;-webkit-appearance:none;">
|
||||
<div class="hint mute" id="hPass2" style="min-height:18px;font-size:12px;margin-top:5px;color:var(--text-dim);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="row" style="display:flex;align-items:center;gap:10px;margin:6px 0 16px;padding:12px;background:var(--bg-elev);border-radius:10px;border:1px solid var(--border);cursor:pointer;">
|
||||
<input type="checkbox" id="b-rem" checked style="width:20px;height:20px;accent-color:var(--primary);flex-shrink:0;">
|
||||
<span style="font-size:14px;font-weight:600;">Запомнить меня (30 дней)</span>
|
||||
</label>
|
||||
<button class="btn" id="el-login" disabled>Войти</button>
|
||||
<button class="btn ghost" id="el-register">Регистрация</button>
|
||||
<div class="sub" id="el-hint" style="margin-top:8px;text-align:center;">Введите email и пароль (≥8 символов)</div>
|
||||
<button class="btn" id="b-go" type="submit" style="width:100%;border:0;border-radius:12px;padding:15px;font:700 16px/1 inherit;cursor:pointer;background:var(--primary);color:#fff;">Войти</button>
|
||||
</form>
|
||||
<div id="b-switch" style="text-align:center;margin-top:14px;font-size:13px;color:var(--text-dim);">Нет аккаунта? <a href="#" id="b-switch-link" style="color:var(--primary);font-weight:600;text-decoration:none;">Регистрация</a></div>
|
||||
</div>
|
||||
<div class="card" id="recent-users-card">
|
||||
<div class="sub">👥 Недавно зарегистрировались:</div>
|
||||
<div id="recent-users-list" class="loader" style="min-height:60px;padding:8px 0;">Загрузка...</div>
|
||||
</div>
|
||||
<div class="card" style="font-size:12px;color:var(--text-dim);">
|
||||
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете
|
||||
<a href="/legal/TERMS.html" target="_blank">соглашение</a>.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('el-login').onclick = () => this.emailLogin();
|
||||
document.getElementById('el-register').onclick = () => this.showEmailRegister();
|
||||
|
||||
const update = () => {
|
||||
const email = document.getElementById('el-email').value.trim();
|
||||
const pass = document.getElementById('el-pass').value;
|
||||
const ok = email.length > 0 && pass.length >= 8;
|
||||
document.getElementById('el-login').disabled = !ok;
|
||||
const hint = document.getElementById('el-hint');
|
||||
if (pass.length > 0 && pass.length < 8) {
|
||||
hint.textContent = 'Пароль слишком короткий (' + pass.length + '/8)';
|
||||
hint.style.color = 'var(--danger)';
|
||||
} else if (ok) {
|
||||
hint.textContent = '✓ Готово — нажми Войти';
|
||||
hint.style.color = 'var(--success)';
|
||||
} else if (pass.length > 0) {
|
||||
hint.textContent = 'Введите email';
|
||||
hint.style.color = 'var(--text-dim)';
|
||||
} else {
|
||||
hint.textContent = 'Введите email и пароль (≥8 символов)';
|
||||
hint.style.color = 'var(--text-dim)';
|
||||
}
|
||||
};
|
||||
document.getElementById('el-email').addEventListener('input', update);
|
||||
document.getElementById('el-pass').addEventListener('input', update);
|
||||
|
||||
// подгрузим недавно зарегистрированных
|
||||
this._initAuthForm();
|
||||
this.loadRecentUsers();
|
||||
},
|
||||
|
||||
// === ОТДЕЛЬНЫЙ ЭКРАН РЕГИСТРАЦИИ ===
|
||||
showEmailRegister() {
|
||||
document.getElementById('app').innerHTML = `
|
||||
<div class="view">
|
||||
<h1>Регистрация</h1>
|
||||
<div class="sub">Создай аккаунт за 30 секунд</div>
|
||||
<div class="card">
|
||||
<label>Имя (как к тебе обращаться)</label>
|
||||
<input class="input" id="rg-name" type="text" placeholder="Саша" autocomplete="given-name">
|
||||
<label>Email</label>
|
||||
<input class="input" id="rg-email" type="email" placeholder="you@example.com" autocomplete="email">
|
||||
<label>Пароль (≥8 символов)</label>
|
||||
<input class="input" id="rg-pass" type="password" placeholder="минимум 8 символов" autocomplete="new-password">
|
||||
<label>Пароль ещё раз</label>
|
||||
<input class="input" id="rg-pass2" type="password" placeholder="повтори пароль" autocomplete="new-password">
|
||||
<label class="checkrow">
|
||||
<input type="checkbox" id="rg-adult" checked>
|
||||
<span>Мне уже есть 18 лет</span>
|
||||
</label>
|
||||
<label class="checkrow">
|
||||
<input type="checkbox" id="rg-terms" checked>
|
||||
<span>Принимаю <a href="/legal/TERMS.html" target="_blank">соглашение</a></span>
|
||||
</label>
|
||||
<label class="checkrow">
|
||||
<input type="checkbox" id="rg-remember" checked>
|
||||
<span>Запомнить меня (30 дней)</span>
|
||||
</label>
|
||||
<button class="btn" id="rg-submit">Создать аккаунт</button>
|
||||
<button class="btn ghost" id="rg-back">← Назад ко входу</button>
|
||||
<div class="sub" id="rg-hint" style="margin-top:8px;text-align:center;"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const update = () => {
|
||||
const name = document.getElementById('rg-name').value.trim();
|
||||
const email = document.getElementById('rg-email').value.trim();
|
||||
const pass = document.getElementById('rg-pass').value;
|
||||
const pass2 = document.getElementById('rg-pass2').value;
|
||||
const adult = document.getElementById('rg-adult').checked;
|
||||
const terms = document.getElementById('rg-terms').checked;
|
||||
const btn = document.getElementById('rg-submit');
|
||||
const hint = document.getElementById('rg-hint');
|
||||
let msg = '';
|
||||
let ready = true;
|
||||
if (name.length < 1) { msg = 'Введите имя'; ready = false; }
|
||||
else if (!email.includes('@') || !email.includes('.')) { msg = 'Некорректный email'; ready = false; }
|
||||
else if (pass.length < 8) { msg = 'Пароль минимум 8 символов (' + pass.length + '/8)'; ready = false; }
|
||||
else if (pass !== pass2) { msg = 'Пароли не совпадают'; ready = false; }
|
||||
else if (!adult) { msg = 'Только для лиц 18+'; ready = false; }
|
||||
else if (!terms) { msg = 'Примите соглашение'; ready = false; }
|
||||
else { msg = '✓ Всё готово'; }
|
||||
btn.disabled = !ready;
|
||||
hint.textContent = msg;
|
||||
hint.style.color = ready ? 'var(--success)' : (msg.startsWith('✓') ? 'var(--success)' : 'var(--text-dim)');
|
||||
};
|
||||
['rg-name','rg-email','rg-pass','rg-pass2','rg-adult','rg-terms'].forEach((id) => {
|
||||
document.getElementById(id).addEventListener('input', update);
|
||||
document.getElementById(id).addEventListener('change', update);
|
||||
});
|
||||
update();
|
||||
|
||||
document.getElementById('rg-submit').onclick = () => this.emailRegister();
|
||||
document.getElementById('rg-back').onclick = () => this.showEmailLogin();
|
||||
},
|
||||
|
||||
async emailRegister() {
|
||||
const name = document.getElementById('rg-name').value.trim();
|
||||
const email = document.getElementById('rg-email').value.trim();
|
||||
const pass = document.getElementById('rg-pass').value;
|
||||
const pass2 = document.getElementById('rg-pass2').value;
|
||||
const adult = document.getElementById('rg-adult').checked;
|
||||
const terms = document.getElementById('rg-terms').checked;
|
||||
const remember = document.getElementById('rg-remember').checked;
|
||||
const btn = document.getElementById('rg-submit');
|
||||
const hint = document.getElementById('rg-hint');
|
||||
|
||||
// повторная проверка на стороне клиента
|
||||
if (!name || !email.includes('@') || pass.length < 8 || pass !== pass2 || !adult || !terms) {
|
||||
hint.textContent = 'Проверьте поля';
|
||||
hint.style.color = 'var(--danger)';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Создаём аккаунт…';
|
||||
hint.textContent = 'Регистрация на сервере…';
|
||||
hint.style.color = 'var(--text-dim)';
|
||||
|
||||
try {
|
||||
const r = await API.Auth.registerEmail({
|
||||
email,
|
||||
password: pass,
|
||||
name,
|
||||
consents: { adult: true, terms: true, privacy: true, disclaimer: true },
|
||||
remember: remember,
|
||||
});
|
||||
if (r && r.tokens) API.Store.save(r.tokens);
|
||||
const me = await API.Auth.me();
|
||||
API.Store.user = me.user || me;
|
||||
btn.textContent = '✓ Готово';
|
||||
hint.textContent = 'Добро пожаловать, ' + name + '!';
|
||||
hint.style.color = 'var(--success)';
|
||||
this.onLogin();
|
||||
await this.maybeOnboard();
|
||||
this.showView('map');
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Создать аккаунт';
|
||||
const msg = (e && e.message) || 'Неизвестная ошибка';
|
||||
hint.textContent = '⚠️ ' + msg;
|
||||
hint.style.color = 'var(--danger)';
|
||||
if (msg.includes('email')) {
|
||||
// подсветить поле email красным
|
||||
const el = document.getElementById('rg-email');
|
||||
if (el) el.style.borderColor = 'var(--danger)';
|
||||
}
|
||||
console.error('register failed:', e);
|
||||
}
|
||||
},
|
||||
|
||||
async loadRecentUsers() {
|
||||
try {
|
||||
const r = await api('/api/v1/users/recent?limit=8');
|
||||
@ -410,22 +284,208 @@ const App = {
|
||||
},
|
||||
|
||||
async emailLogin() {
|
||||
const email = document.getElementById('el-email').value.trim();
|
||||
const pass = document.getElementById('el-pass').value;
|
||||
const remember = document.getElementById('el-remember')?.checked ?? true;
|
||||
// старая функция — оставлена как fallback, реальный submit через _initAuthForm
|
||||
return this._submitAuth('in');
|
||||
},
|
||||
|
||||
// === ОБНОВЛЁННЫЙ UX (от Grok) ===
|
||||
_initAuthForm() {
|
||||
const app = this;
|
||||
let mode = 'in'; // 'in' | 'up'
|
||||
let busy = false;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function setMode(m) {
|
||||
mode = m;
|
||||
const up = m === 'up';
|
||||
// табы
|
||||
$('tIn').style.background = up ? 'transparent' : 'var(--primary)';
|
||||
$('tIn').style.color = up ? 'var(--text-dim)' : '#fff';
|
||||
$('tUp').style.background = up ? 'var(--primary)' : 'transparent';
|
||||
$('tUp').style.color = up ? '#fff' : 'var(--text-dim)';
|
||||
// поля
|
||||
$('regBox').style.display = up ? '' : 'none';
|
||||
$('regBox2').style.display = up ? '' : 'none';
|
||||
$('b-pass').autocomplete = up ? 'new-password' : 'current-password';
|
||||
// кнопка
|
||||
btnState('', up ? 'Создать аккаунт' : 'Войти');
|
||||
// switch
|
||||
$('b-switch').innerHTML = up
|
||||
? 'Уже есть аккаунт? <a href="#" id="b-switch-link" style="color:var(--primary);font-weight:600;text-decoration:none;">Войти</a>'
|
||||
: 'Нет аккаунта? <a href="#" id="b-switch-link" style="color:var(--primary);font-weight:600;text-decoration:none;">Регистрация</a>';
|
||||
$('b-switch-link').onclick = (e) => { e.preventDefault(); setMode(up ? 'in' : 'up'); };
|
||||
// фокус
|
||||
clearMarks();
|
||||
validate(false);
|
||||
setTimeout(() => { $(up ? 'b-name' : 'b-email')?.focus(); }, 50);
|
||||
}
|
||||
|
||||
function mark(inp, h, ok, msg) {
|
||||
inp.classList.remove('bad', 'good');
|
||||
h.className = 'hint ' + (msg ? (ok ? 'ok' : 'err') : 'mute');
|
||||
if (msg) { inp.classList.add(ok ? 'good' : 'bad'); h.textContent = (ok ? '✓ ' : '✗ ') + msg; }
|
||||
else h.textContent = '';
|
||||
return ok;
|
||||
}
|
||||
|
||||
function vName(show) {
|
||||
const v = $('b-name').value.trim();
|
||||
if (!v) return mark($('b-name'), $('hName'), false, show ? 'Введите имя' : '');
|
||||
if (v.length < 2) return mark($('b-name'), $('hName'), false, 'Минимум 2 буквы');
|
||||
return mark($('b-name'), $('hName'), true, 'Отлично');
|
||||
}
|
||||
function vEmail(show) {
|
||||
const v = $('b-email').value.trim();
|
||||
if (!v) return mark($('b-email'), $('hEmail'), false, show ? 'Введите email' : '');
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return mark($('b-email'), $('hEmail'), false, 'Некорректный email');
|
||||
return mark($('b-email'), $('hEmail'), true, 'Похоже верно');
|
||||
}
|
||||
function vPass(show) {
|
||||
const v = $('b-pass').value;
|
||||
if (!v) return mark($('b-pass'), $('hPass'), false, show ? 'Введите пароль' : '');
|
||||
if (v.length < 6) return mark($('b-pass'), $('hPass'), false, 'Ещё ' + (6 - v.length) + ' симв.');
|
||||
return mark($('b-pass'), $('hPass'), true, 'Надёжно');
|
||||
}
|
||||
function vPass2(show) {
|
||||
const v = $('b-pass2').value;
|
||||
if (!v) return mark($('b-pass2'), $('hPass2'), false, show ? 'Повторите пароль' : '');
|
||||
if (v !== $('b-pass').value) return mark($('b-pass2'), $('hPass2'), false, 'Пароли не совпадают');
|
||||
return mark($('b-pass2'), $('hPass2'), true, 'Совпадают');
|
||||
}
|
||||
function clearMarks() {
|
||||
['b-name', 'b-email', 'b-pass', 'b-pass2'].forEach((id) => $(id)?.classList.remove('bad', 'good'));
|
||||
['hName', 'hEmail', 'hPass', 'hPass2'].forEach((id) => {
|
||||
const h = $(id); if (h) { h.className = 'hint mute'; h.textContent = ''; }
|
||||
});
|
||||
}
|
||||
function validate(show) {
|
||||
let ok = vEmail(show) & vPass(show);
|
||||
if (mode === 'up') ok = vName(show) & ok & vPass2(show);
|
||||
return !!ok;
|
||||
}
|
||||
|
||||
function btnState(s, txt) {
|
||||
const b = $('b-go');
|
||||
b.className = 'btn' + (s ? ' ' + s : '');
|
||||
b.textContent = txt;
|
||||
b.disabled = !!busy;
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
let t = document.getElementById('b-toast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.id = 'b-toast';
|
||||
t.style.cssText = 'position:fixed;left:50%;bottom:88px;transform:translateX(-50%) translateY(80px);background:#222;color:#fff;padding:12px 18px;border-radius:12px;font-size:14px;font-weight:600;box-shadow:0 8px 28px #0008;opacity:0;transition:.28s;z-index:99;max-width:90%;border:1px solid var(--border);';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg;
|
||||
requestAnimationFrame(() => {
|
||||
t.style.opacity = '1';
|
||||
t.style.transform = 'translateX(-50%) translateY(0)';
|
||||
});
|
||||
setTimeout(() => {
|
||||
t.style.opacity = '0';
|
||||
t.style.transform = 'translateX(-50%) translateY(80px)';
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
// bind
|
||||
$('tIn').onclick = () => setMode('in');
|
||||
$('tUp').onclick = () => setMode('up');
|
||||
$('b-switch-link').onclick = (e) => { e.preventDefault(); setMode(mode === 'up' ? 'in' : 'up'); };
|
||||
['input', 'blur'].forEach((ev) => {
|
||||
$('b-name')?.addEventListener(ev, () => { if (mode === 'up') vName(true); });
|
||||
$('b-email')?.addEventListener(ev, () => vEmail(true));
|
||||
$('b-pass')?.addEventListener(ev, () => { vPass(true); if (mode === 'up' && $('b-pass2').value) vPass2(true); });
|
||||
$('b-pass2')?.addEventListener(ev, () => { if (mode === 'up') vPass2(true); });
|
||||
});
|
||||
|
||||
$('b-auth-form').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
if (!validate(true)) {
|
||||
btnState('err', '⚠️ Проверьте поля');
|
||||
setTimeout(() => btnState('', mode === 'up' ? 'Создать аккаунт' : 'Войти'), 1400);
|
||||
const bad = $('b-auth-form').querySelector('input.bad');
|
||||
if (bad) bad.focus();
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
btnState('load', mode === 'up' ? '⏳ Создаём...' : '⏳ Входим...');
|
||||
try {
|
||||
await app._submitAuth(mode);
|
||||
} catch (err) {
|
||||
busy = false;
|
||||
const msg = (err && err.message) || 'Ошибка';
|
||||
btnState('err', '⚠️ ' + msg);
|
||||
const field = (err && err.data && err.data.field) || '';
|
||||
if (field === 'email') { $('b-email').classList.add('bad'); mark($('b-email'), $('hEmail'), false, msg); $('b-email').focus(); }
|
||||
else if (field === 'pass') { $('b-pass').classList.add('bad'); mark($('b-pass'), $('hPass'), false, msg); $('b-pass').focus(); }
|
||||
else if (field === 'name') { $('b-name').classList.add('bad'); mark($('b-name'), $('hName'), false, msg); $('b-name').focus(); }
|
||||
setTimeout(() => btnState('', mode === 'up' ? 'Создать аккаунт' : 'Войти'), 2000);
|
||||
}
|
||||
// busy снимется в _submitAuth при успехе (showView уйдёт)
|
||||
};
|
||||
|
||||
// экспортируем для возможных других вызовов
|
||||
app._authSwitchMode = setMode;
|
||||
setMode('in');
|
||||
},
|
||||
|
||||
async _submitAuth(mode) {
|
||||
const email = document.getElementById('b-email').value.trim();
|
||||
const pass = document.getElementById('b-pass').value;
|
||||
const remember = document.getElementById('b-rem').checked;
|
||||
if (mode === 'up') {
|
||||
const name = document.getElementById('b-name').value.trim();
|
||||
const pass2 = document.getElementById('b-pass2').value;
|
||||
if (!name || pass !== pass2) throw new Error('Проверьте поля');
|
||||
const r = await API.Auth.registerEmail({
|
||||
email, password: pass, name,
|
||||
consents: { adult: true, terms: true, privacy: true, disclaimer: true },
|
||||
remember: remember,
|
||||
});
|
||||
if (r && r.tokens) API.Store.save(r.tokens);
|
||||
const me = await API.Auth.me();
|
||||
API.Store.user = me.user || me;
|
||||
const btn = document.getElementById('b-go');
|
||||
btn.className = 'btn ok';
|
||||
btn.textContent = '✓ Готово!';
|
||||
this._toast('Добро пожаловать, ' + name + '!');
|
||||
this.onLogin();
|
||||
await this.maybeOnboard();
|
||||
setTimeout(() => this.showView('map'), 700);
|
||||
} else {
|
||||
const r = await API.Auth.loginEmail(email, pass, remember);
|
||||
if (r && r.tokens) API.Store.save(r.tokens);
|
||||
const me = await API.Auth.me();
|
||||
API.Store.user = me.user || me;
|
||||
const btn = document.getElementById('b-go');
|
||||
btn.className = 'btn ok';
|
||||
btn.textContent = '✓ Готово!';
|
||||
const nm = me.user?.name || (me.user && me.user.email) || email.split('@')[0];
|
||||
this._toast('С возвращением, ' + nm + '!');
|
||||
this.onLogin();
|
||||
this.maybeOnboard();
|
||||
this.showView('map');
|
||||
} catch (e) {
|
||||
TG_APP.showAlert('Ошибка: ' + e.message);
|
||||
setTimeout(() => this.showView('map'), 500);
|
||||
}
|
||||
},
|
||||
|
||||
_toast(msg) {
|
||||
let t = document.getElementById('b-toast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.id = 'b-toast';
|
||||
t.style.cssText = 'position:fixed;left:50%;bottom:88px;transform:translateX(-50%) translateY(80px);background:#222;color:#fff;padding:12px 18px;border-radius:12px;font-size:14px;font-weight:600;box-shadow:0 8px 28px #0008;opacity:0;transition:.28s;z-index:99;max-width:90%;border:1px solid var(--border);';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg;
|
||||
requestAnimationFrame(() => { t.style.opacity = '1'; t.style.transform = 'translateX(-50%) translateY(0)'; });
|
||||
setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateX(-50%) translateY(80px)'; }, 2200);
|
||||
},
|
||||
|
||||
showEmailRegister() { this.showEmailLogin(); this._authSwitchMode && this._authSwitchMode('up'); },
|
||||
|
||||
async maybeOnboard() {
|
||||
if (this._onboardRunning) return;
|
||||
this._onboardRunning = true;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user