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 */
|
/* edit marker */
|
||||||
.bubble .edited { font-style: italic; opacity: 0.7; }
|
.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() {
|
showEmailLogin() {
|
||||||
document.getElementById('app').innerHTML = `
|
document.getElementById('app').innerHTML = `
|
||||||
<div class="view">
|
<div class="view">
|
||||||
<h1>Вход</h1>
|
<div class="wrap" style="padding:0;">
|
||||||
<div class="sub">Войди или зарегистрируйся по email</div>
|
<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">
|
<div class="card">
|
||||||
<label>Email</label>
|
<form id="b-auth-form" novalidate>
|
||||||
<input class="input" id="el-email" type="email" placeholder="you@example.com" autocomplete="email">
|
<div id="regBox" style="display:none;">
|
||||||
<label>Пароль (≥8 символов)</label>
|
<div class="field" style="margin-bottom:14px;">
|
||||||
<input class="input" id="el-pass" type="password" placeholder="минимум 8 символов" autocomplete="current-password">
|
<label for="b-name" style="display:block;font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 6px;">Имя</label>
|
||||||
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:14px;">
|
<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;">
|
||||||
<input type="checkbox" id="el-remember" checked>
|
<div class="hint mute" id="hName" style="min-height:18px;font-size:12px;margin-top:5px;color:var(--text-dim);"></div>
|
||||||
<span>Запомнить меня (30 дней)</span>
|
</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>
|
</label>
|
||||||
<button class="btn" id="el-login" disabled>Войти</button>
|
<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>
|
||||||
<button class="btn ghost" id="el-register">Регистрация</button>
|
</form>
|
||||||
<div class="sub" id="el-hint" style="margin-top:8px;text-align:center;">Введите email и пароль (≥8 символов)</div>
|
<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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('el-login').onclick = () => this.emailLogin();
|
this._initAuthForm();
|
||||||
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.loadRecentUsers();
|
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() {
|
async loadRecentUsers() {
|
||||||
try {
|
try {
|
||||||
const r = await api('/api/v1/users/recent?limit=8');
|
const r = await api('/api/v1/users/recent?limit=8');
|
||||||
@ -410,22 +284,208 @@ const App = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async emailLogin() {
|
async emailLogin() {
|
||||||
const email = document.getElementById('el-email').value.trim();
|
// старая функция — оставлена как fallback, реальный submit через _initAuthForm
|
||||||
const pass = document.getElementById('el-pass').value;
|
return this._submitAuth('in');
|
||||||
const remember = document.getElementById('el-remember')?.checked ?? true;
|
},
|
||||||
|
|
||||||
|
// === ОБНОВЛЁННЫЙ 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 {
|
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);
|
const r = await API.Auth.loginEmail(email, pass, remember);
|
||||||
if (r && r.tokens) API.Store.save(r.tokens);
|
if (r && r.tokens) API.Store.save(r.tokens);
|
||||||
const me = await API.Auth.me();
|
const me = await API.Auth.me();
|
||||||
API.Store.user = me.user || 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.onLogin();
|
||||||
this.maybeOnboard();
|
setTimeout(() => this.showView('map'), 500);
|
||||||
this.showView('map');
|
|
||||||
} catch (e) {
|
|
||||||
TG_APP.showAlert('Ошибка: ' + e.message);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_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() {
|
async maybeOnboard() {
|
||||||
if (this._onboardRunning) return;
|
if (this._onboardRunning) return;
|
||||||
this._onboardRunning = true;
|
this._onboardRunning = true;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user