// Profile view (свой профиль) const ProfileView = { drinks: new Set(), activities: new Set(), purposes: new Set(), anonymous: false, DRINKS: [ ['beer', '🍺 Пиво'], ['wine', '🍷 Вино'], ['whiskey', '🥃 Виски'], ['vodka', '🍸 Водка'], ['cocktail', '🍹 Коктейль'], ['rum', '🥃 Ром'], ['gin', '🍸 Джин'], ['tequila', '🌵 Текила'], ['champagne', '🍾 Шампанское'], ['non_alcoholic', '🥤 Безалкогольное'], ], ACTIVITIES: [ ['walk', '🚶 Прогулка'], ['dinner', '🍽️ Ужин'], ['bar', '🍻 Бар'], ['cafe', '☕ Кафе'], ['travel', '✈️ Путешествие'], ['movie', '🎬 Кино'], ['concert', '🎵 Концерт'], ['sport', '⚽ Спорт'], ['gaming', '🎮 Игры'], ['reading', '📚 Чтение'], ], PURPOSES: [ ['chat', '💬 Пообщаться'], ['drink', '🍻 Выпить'], ['walk', '🌳 Погулять'], ['dinner', '🍽️ Поужинать'], ['travel', '✈️ Поехать'], ['friendship', '🤝 Дружба'], ['relationship', '❤️ Отношения'], ], async render(root) { try { const me = await API.Auth.me(); API.Store.user = me.user; const prefs = me.prefs || {}; this.drinks = new Set(prefs.drinks || []); this.activities = new Set(prefs.activities || []); this.purposes = new Set(prefs.purposes || []); this.renderForm(root, me.user); } catch (e) { root.innerHTML = '
Не удалось загрузить: ' + e.message + '
'; } }, renderForm(root, u) { root.innerHTML = `

👤 Мой профиль

${escapeHtml(((u.name || u.email || '?')[0] || '?').toUpperCase())}
${escapeHtml(u.name || 'Без имени')}
${u.telegram_id ? `
@${escapeHtml(u.username || '')}
` : `
${escapeHtml(u.email || '')}
`}
🍻 Что пью
🎯 Чем занимаюсь
💡 Цели
`; this.renderChips('p-drinks', this.DRINKS, this.drinks); this.renderChips('p-activities', this.ACTIVITIES, this.activities); this.renderChips('p-purposes', this.PURPOSES, this.purposes); // upload photo document.getElementById('p-upload').onclick = () => document.getElementById('p-photo-input').click(); document.getElementById('p-photo-input').onchange = async (e) => { const f = e.target.files[0]; if (!f) return; if (f.size > 8 * 1024 * 1024) { App._toast('Файл больше 8 МБ', 'err'); return; } if (!/^image\/(jpeg|png|webp)$/.test(f.type)) { App._toast('Только JPEG/PNG/WebP', 'err'); return; } const fd = new FormData(); fd.append('photo', f); const btn = document.getElementById('p-upload'); const orig = btn.textContent; btn.textContent = '⏳ Загружаем…'; btn.classList.add('load'); try { const r = await fetch(API_BASE + '/api/v1/me/photo', { method: 'POST', body: fd, credentials: 'include' }); if (!r.ok) throw new Error((await r.json()).error || 'HTTP ' + r.status); App._toast('✓ Фото загружено', 'ok'); } catch (e) { App._toast('⚠️ ' + e.message, 'err'); } finally { btn.textContent = orig; btn.classList.remove('load'); } }; // live counters const bio = document.getElementById('p-bio'); const counter = document.getElementById('h-bio'); function updBio() { const n = bio.value.length; counter.textContent = n + ' / 500'; counter.className = 'hint ' + (n > 450 ? 'err' : 'mute'); } bio.addEventListener('input', updBio); updBio(); document.getElementById('p-save').onclick = () => this.save(); }, renderChips(elId, items, selected) { const el = document.getElementById(elId); if (!el) return; items.forEach(([key, label]) => { const chip = document.createElement('span'); chip.className = 'chip' + (selected.has(key) ? ' on' : ''); chip.textContent = label; chip.onclick = () => { try { if (selected.has(key)) selected.delete(key); else selected.add(key); chip.classList.toggle('on'); TG_APP.haptic?.('selection'); } catch (e) { console.warn('chip click err', e); } }; el.appendChild(chip); }); }, async save() { const btn = document.getElementById('p-save'); const orig = btn.textContent; btn.className = 'btn-b load'; btn.textContent = '⏳ Сохраняем…'; try { await API.Auth.updateMe({ name: document.getElementById('p-name').value.trim(), city: document.getElementById('p-city').value.trim(), bio: document.getElementById('p-bio').value.trim(), gender: document.getElementById('p-gender').value, }); await API.Auth.updatePrefs({ drinks: [...this.drinks], activities: [...this.activities], purposes: [...this.purposes], }); TG_APP.haptic?.('notification'); btn.className = 'btn-b ok'; btn.textContent = '✓ Готово!'; App._toast('Профиль обновлён', 'ok'); setTimeout(() => { btn.className = 'btn-b'; btn.textContent = orig; }, 1500); } catch (e) { btn.className = 'btn-b err'; btn.textContent = '⚠️ ' + (e.message || 'Ошибка'); App._toast('Не удалось сохранить', 'err'); const bad = (e.message || '').includes('name') ? 'p-name' : (e.message || '').includes('gender') ? 'p-gender' : null; if (bad) document.getElementById(bad)?.classList.add('bad'); setTimeout(() => { btn.className = 'btn-b'; btn.textContent = orig; }, 2000); } }, }; window.ProfileView = ProfileView; // UserProfileView — чужой профиль (read-only) — открывается по тапу на маркер на карте const UserProfileView = { userId: null, userName: null, data: null, async render(root, params) { this.userId = params.userId; this.userName = params.name || 'Пользователь'; console.log('[UserProfile] render start, userId:', this.userId, 'name:', this.userName, 'params:', JSON.stringify(params)); root.innerHTML = `

👤 ${escapeHtml(this.userName)}

${'
⏳ Загружаю…
'}
`; try { const [user, stats] = await Promise.all([ API.UserApi.get(this.userId), API.ReviewApi.stats(this.userId).catch(() => ({})), ]); console.log('[UserProfile] loaded: user:', JSON.stringify(user).slice(0,200), 'stats:', JSON.stringify(stats)); this.data = user.user || user; const u = this.data; const init = ((u.name || u.email || '?')[0] || '?').toUpperCase(); const rating = stats.rating_avg ? Number(stats.rating_avg).toFixed(1) : '—'; const reviewsCount = stats.rating_count || 0; document.getElementById('up-content').innerHTML = `
${u.photo ? '' : init}
${escapeHtml(u.name || '')}
${u.city ? `
📍 ${escapeHtml(u.city)}
` : ''} ${u.age ? `
🎂 ${u.age} лет
` : ''} ${reviewsCount > 0 ? `
★ ${rating} · ${reviewsCount} ${pluralRu(reviewsCount, 'отзыв', 'отзыва', 'отзывов')}
` : ''}
${u.bio ? `
О себе
${escapeHtml(u.bio)}
` : ''}
${reviewsCount > 0 ? `` : ''}
`; const msgBtn = document.getElementById('up-message'); if (msgBtn) msgBtn.onclick = async () => { msgBtn.className = 'btn-b load'; msgBtn.textContent = '⏳ Открываем…'; try { const r = await API.ChatApi.ensureChat(this.userId); const cid = r.chat_id || r.chat?.id; if (!cid) throw new Error('chat not created'); App.openChat(cid, this.userId, u.name); } catch (e) { msgBtn.className = 'btn-b err'; msgBtn.textContent = '⚠️ ' + (e.message || 'Ошибка'); App._toast('Не удалось открыть чат', 'err'); setTimeout(() => { msgBtn.className = 'btn-b'; msgBtn.textContent = '💬 Написать'; }, 2000); } }; const revBtn = document.getElementById('up-reviews'); if (revBtn) revBtn.onclick = () => UserReviewsView.render(root, { userId: this.userId, userName: u.name }); } catch (e) { console.error('[UserProfile] load failed:', e && e.message, 'userId:', this.userId, 'stack:', e && e.stack); // ship to backend debug log try { fetch('/api/v1/debug/log', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 'UserProfileError', msg: String(e && e.message), userId: String(this.userId), stack: String(e && e.stack || '').slice(0, 500) }) }); } catch {} document.getElementById('up-content').innerHTML = `
⚠️ Не удалось загрузить
`; App._toast('Ошибка загрузки профиля', 'err'); } }, }; window.UserProfileView = UserProfileView; function pluralRu(n, one, few, many) { const mod10 = n % 10, mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) return one; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few; return many; }