// Main app controller
const App = {
consentGiven: false,
isMobileMode: false,
_preReg: null, // callback после согласия
async init() {
const isTouch = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
const smallScreen = window.innerWidth <= 768;
const savedMode = localStorage.getItem('buhapp.mode');
if (savedMode === 'desktop') {
this.isMobileMode = false;
} else if (savedMode === 'mobile') {
this.isMobileMode = true;
} else {
this.isMobileMode = !smallScreen;
}
this.applyMode();
document.getElementById('mode-toggle').onclick = () => {
this.isMobileMode = !this.isMobileMode;
localStorage.setItem('buhapp.mode', this.isMobileMode ? 'mobile' : 'desktop');
this.applyMode();
if (window.MapView?.map) {
setTimeout(() => window.MapView.map.invalidateSize(), 100);
}
};
document.querySelectorAll('.tab').forEach(btn => {
btn.onclick = () => this.showView(btn.dataset.view);
});
if (TG_APP.isTelegram && TG_APP.user) {
try {
await API.Auth.loginWithTelegram(TG_APP.initData);
await API.Auth.me();
await this.checkConsent();
await this.maybeOnboard();
this.showView('map');
} catch (e) {
this.showError('Не удалось войти через Telegram: ' + e.message);
}
} else if (API.Store.access) {
try {
await API.Auth.me();
await this.checkConsent();
this.showView('map');
} catch (e) {
API.Store.clear();
this.showError('Сессия истекла: ' + e.message);
}
} else {
// Вне Telegram — показать email login (но сначала согласия)
this.showWelcome();
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && document.activeElement?.id === 'msg-input') return;
if (e.key === 'Escape') {
if (window.ChatView?.cleanup) window.ChatView.cleanup();
if (API.Store.access) this.showView('map');
}
});
},
applyMode() {
if (this.isMobileMode) {
document.body.classList.add('mobile-mode');
document.getElementById('mode-toggle').textContent = '🖥️ На весь экран';
} else {
document.body.classList.remove('mobile-mode');
document.getElementById('mode-toggle').textContent = '📱 Мобильный';
}
},
async checkConsent() {
try {
const s = await API.ConsentApi.status();
if (s.all_accepted) return true;
} catch (e) {
console.warn('consent status check failed', e);
}
// не показываем если нет авторизации — это для авторизованных
if (!API.Store.user) return true;
this.showConsent();
return false;
},
showConsent() {
const ov = document.createElement('div');
ov.id = 'consent-overlay';
ov.innerHTML = `
🍻 Добро пожаловать в BuhApp
Приложение для поиска компании. Только для лиц 18+.
`;
// цепляемся к #mobile-frame если он есть
const target = document.querySelector('body.mobile-mode #mobile-frame') || document.body;
target.appendChild(ov);
const check = () => {
const ok = document.getElementById('c-adult').checked
&& document.getElementById('c-terms').checked
&& document.getElementById('c-privacy').checked
&& document.getElementById('c-disclaimer').checked;
document.getElementById('c-submit').disabled = !ok;
};
['c-adult','c-terms','c-privacy','c-disclaimer'].forEach(id =>
document.getElementById(id).addEventListener('change', check)
);
document.getElementById('c-submit').onclick = async () => {
try {
await API.ConsentApi.accept({ adult: true, terms: true, privacy: true, disclaimer: true });
ov.remove();
this.consentGiven = true;
if (this._preReg) {
const fn = this._preReg;
this._preReg = null;
fn();
} else {
await this.maybeOnboard();
this.showView('map');
}
} catch (e) {
TG_APP.showAlert('Ошибка: ' + e.message);
}
};
},
showError(msg) {
document.getElementById('app').innerHTML = `
`;
},
showWelcome() {
// Приветственный экран вне Telegram — выбор: email login или открыть в Telegram
document.getElementById('app').innerHTML = `
🍻 BuhApp
Приложение для поиска компании (18+)
BuhApp работает внутри Telegram — это безопаснее, проще и быстрее.
🚀 Открыть в Telegram
`;
document.getElementById('use-email').onclick = () => this.showEmailLogin();
},
showEmailLogin() {
document.getElementById('app').innerHTML = `
Вход
Войди или зарегистрируйся по email
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете
соглашение.
`;
document.getElementById('el-login').onclick = () => this.emailLogin();
document.getElementById('el-register').onclick = () => this.emailRegisterPre();
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;
document.getElementById('el-register').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 {
hint.textContent = 'Введите email и пароль';
hint.style.color = 'var(--text-dim)';
}
};
document.getElementById('el-email').addEventListener('input', update);
document.getElementById('el-pass').addEventListener('input', update);
},
// Сначала consent overlay, потом реальная регистрация
emailRegisterPre() {
const email = document.getElementById('el-email').value.trim();
const pass = document.getElementById('el-pass').value;
if (!email || pass.length < 8) {
TG_APP.showAlert('Email и пароль (≥8 символов) обязательны');
return;
}
// проверим дубликат email
this._preReg = () => this.emailRegister();
this.showConsent();
},
async emailLogin() {
const email = document.getElementById('el-email').value.trim();
const pass = document.getElementById('el-pass').value;
try {
await API.Auth.loginEmail(email, pass);
await API.Auth.me();
// для email-логина согласия можно проверить, но юзер их уже дал
await this.maybeOnboard();
this.showView('map');
} catch (e) {
TG_APP.showAlert('Ошибка: ' + e.message);
}
},
async emailRegister() {
const email = document.getElementById('el-email').value.trim();
const pass = document.getElementById('el-pass').value;
// показываем сообщение о процессе
const ov = document.createElement('div');
ov.id = 'consent-overlay';
ov.innerHTML = `
`;
const target = document.querySelector('body.mobile-mode #mobile-frame') || document.body;
target.appendChild(ov);
try {
const r = await API.Auth.registerEmail({
email,
password: pass,
name: email.split('@')[0],
consents: { adult: true, terms: true, privacy: true, disclaimer: true },
});
await API.Auth.me();
ov.remove();
await this.maybeOnboard();
this.showView('map');
} catch (e) {
ov.remove();
TG_APP.showAlert('Ошибка регистрации: ' + (e?.message || 'unknown'));
// вернёмся к форме
this.showEmailLogin();
}
},
async maybeOnboard() {
if (this._onboardRunning) return;
this._onboardRunning = true;
try {
if (!window.OnboardingView) return;
if (OnboardingView.isDone()) return;
await OnboardingView.render(document.body);
} finally {
this._onboardRunning = false;
}
},
showView(name, params = {}) {
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === name));
const root = document.getElementById('app');
if (window.ChatView?.cleanup) window.ChatView.cleanup();
switch (name) {
case 'map': MapView.render(root); break;
case 'chats': ChatsView.render(root); break;
case 'chat': ChatView.render(root, params); break;
case 'profile': ProfileView.render(root); break;
case 'settings': SettingsView.render(root); break;
case 'review': ReviewView.render(root, params); break;
case 'user-reviews': UserReviewsView.render(root, params); break;
case 'user-profile': UserProfileView.render(root, params); break;
default: root.innerHTML = 'Неизвестный экран
';
}
},
openChat(chatId, otherId, otherName) {
this.showView('chat', { chatId, otherId, otherName });
},
openReview(chatId, otherId, otherName) {
this.showView('review', { chatId, otherId, otherName });
},
openUserProfile(userId, name) {
this.showView('user-profile', { userId, name });
},
toast(msg) {
const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2000);
},
};
// helpers
function escapeHtml(s) {
if (!s) return '';
return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
}
// Debug-оверлей: показывает ошибки JS прямо на экране (для отладки в обычном браузере)
window.addEventListener('error', (e) => {
console.error('Global error:', e);
showDebugError(e.message + ' @ ' + (e.filename || '?') + ':' + (e.lineno || '?'));
});
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled rejection:', e);
showDebugError('Promise: ' + (e.reason && e.reason.message ? e.reason.message : (e.reason || 'unknown')));
});
function showDebugError(msg) {
try {
let d = document.getElementById('debug-errors');
if (!d) {
d = document.createElement('div');
d.id = 'debug-errors';
d.style.cssText = 'position:fixed;top:0;left:0;right:0;max-height:50vh;overflow:auto;background:#900;color:#fff;padding:12px;font:12px monospace;z-index:99999;white-space:pre-wrap;';
document.body.appendChild(d);
}
const line = document.createElement('div');
line.textContent = msg;
d.appendChild(line);
} catch {}
}
document.addEventListener('DOMContentLoaded', () => {
App.init().catch((e) => {
console.error('App.init failed:', e);
showDebugError('App.init: ' + (e && e.message ? e.message : String(e)));
});
});