// 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 || 'Пользователь';
root.innerHTML = `
👤 ${escapeHtml(this.userName)}
`;
try {
const [user, stats] = await Promise.all([
API.UserApi.get(this.userId),
API.ReviewApi.stats(this.userId).catch(() => ({})),
]);
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_url ? '' : init}
${escapeHtml(u.name || '')}
${u.city ? `
📍 ${escapeHtml(u.city)}
` : ''}
${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) {
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;
}