// Main app controller const App = { consentGiven: false, isMobileMode: false, async init() { // Определяем: десктоп — стартуем в mobile-mode для удобства разработки 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); } }; // bind tabbar document.querySelectorAll('.tab').forEach(btn => { btn.onclick = () => this.showView(btn.dataset.view); }); // restore tokens from localStorage API.Store.load(); if (TG_APP.isAvailable && TG_APP.initData) { // авторизация через Telegram try { await API.Auth.loginWithTelegram(TG_APP.initData); 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(); await this.maybeOnboard(); this.showView('map'); } catch (e) { API.Store.clear(); this.showError('Сессия истекла: ' + e.message); } } else { // WebApp открыт вне Telegram — предложим email login this.showEmailLogin(); } // bind input events for chat document.body.addEventListener('click', (e) => { const sendBtn = e.target.closest('#msg-send'); if (sendBtn) { window.ChatView.send(); return; } const cancel = e.target.closest('#edit-cancel'); if (cancel) { document.getElementById('msg-input').value = ''; document.getElementById('edit-bar').style.display='none'; return; } }); document.body.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.id === 'msg-input') { if (!e.shiftKey) { e.preventDefault(); window.ChatView.send(); } } }); }, async checkConsent() { // проверяем на бэкенде — если все 4 уже приняты, не показываем try { const s = await API.ConsentApi.status(); if (s.all_accepted) { return true; } } catch (e) { console.warn('consent status check failed', e); } this.showConsent(); return false; }, // Показываем welcome-flow новому пользователю ОДИН раз. // Возвращает Promise, который резолвится когда пользователь прошёл/пропустил онбординг. async maybeOnboard() { if (!window.OnboardingView) { console.warn('[onboarding] OnboardingView not loaded'); return; } if (OnboardingView.isDone()) return; const root = document.getElementById('app'); await OnboardingView.render(root); }, showConsent() { const ov = document.createElement('div'); ov.id = 'consent-overlay'; ov.innerHTML = `

🍻 Добро пожаловать в BuhApp

Приложение для поиска компании. Только для лиц 18+.

⚠️ Внимание: BuhApp — это платформа для знакомств и встреч с другими пользователями. Включает в себя риски, связанные с употреблением алкоголя и личными встречами.

Пожалуйста, ознакомьтесь и примите следующие документы:

`; document.body.appendChild(ov); document.getElementById('c-accept').onclick = async () => { const a = document.getElementById('c-adult').checked; const t = document.getElementById('c-terms').checked; const p = document.getElementById('c-privacy').checked; const d = document.getElementById('c-disclaimer').checked; if (!a || !t || !p || !d) { TG_APP.showAlert('Нужно принять все 4 пункта'); return; } try { await API.api('/api/v1/auth/consents', { method: 'POST', body: { adult: a, terms: t, privacy: p, disclaimer: d, }, }); TG_APP.haptic('notification'); ov.remove(); this.consentGiven = true; await this.maybeOnboard(); this.showView('map'); } catch (e) { TG_APP.showAlert('Ошибка: ' + e.message); } }; }, showError(msg) { document.getElementById('app').innerHTML = `
⚠️
${msg}
`; }, 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 = '📱 Мобильный'; } }, showEmailLogin() { document.getElementById('app').innerHTML = `

🍻 BuhApp

Приложение для поиска компании (Telegram Mini App)
⚠️ WebApp лучше открывать через Telegram — там авторизация автоматическая. Здесь можно зайти по email для тестирования.
Введите email и пароль
🍻 BuhApp — приложение для лиц 18+. Встречи и употребление алкоголя — на свой риск. Принимая соглашение, вы подтверждаете совершеннолетие.
`; document.getElementById('el-login').onclick = () => this.emailLogin(); document.getElementById('el-register').onclick = () => this.emailRegister(); // live-валидация 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); }, 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(); await this.checkConsent(); 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; if (!email || pass.length < 8) { TG_APP.showAlert('Email и пароль (≥8 символов) обязательны'); return; } // disable на время запроса const btn = document.getElementById('el-register'); btn.disabled = true; btn.textContent = 'Регистрация...'; try { console.log('registerEmail:', email); const r = await API.Auth.registerEmail({ email, password: pass, name: email.split('@')[0], consents: { adult: true, terms: true, privacy: true, disclaimer: true }, }); console.log('registerEmail result:', r); await API.Auth.me(); this.consentGiven = true; await this.maybeOnboard(); this.showView('map'); } catch (e) { console.error('registerEmail error:', e); btn.disabled = false; btn.textContent = 'Регистрация'; // покажем ошибку явно const msg = e?.message || 'unknown error'; if (e?.data) console.error('error data:', e.data); TG_APP.showAlert('Ошибка регистрации: ' + msg + '\n\nОткрой DevTools (F12) → Console для подробностей.'); } }, 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])); } function formatTime(iso) { if (!iso) return ''; const d = new Date(iso); const today = new Date(); if (d.toDateString() === today.toDateString()) { return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0'); } return d.getDate().toString().padStart(2, '0') + '.' + (d.getMonth() + 1).toString().padStart(2, '0'); } function formatDate(iso) { if (!iso) return ''; const d = new Date(iso); return d.getDate().toString().padStart(2, '0') + '.' + (d.getMonth() + 1).toString().padStart(2, '0') + '.' + d.getFullYear(); } window.App = App; document.addEventListener('DOMContentLoaded', () => App.init());