buhapp-telegram/public/js/views/profile.js

296 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = '<div class="view"><div class="empty">Не удалось загрузить: ' + e.message + '</div></div>';
}
},
renderForm(root, u) {
root.innerHTML = `
<div class="view">
<h1 style="margin:0 0 14px;font-size:22px;">👤 Мой профиль</h1>
<!-- аватар -->
<div class="card-b" style="display:flex;align-items:center;gap:14px;">
<div class="avatar-b lg" id="p-avatar">${escapeHtml(((u.name || u.email || '?')[0] || '?').toUpperCase())}</div>
<div style="flex:1;">
<div style="font-weight:600;font-size:16px;">${escapeHtml(u.name || 'Без имени')}</div>
${u.telegram_id ? `<div style="font-size:13px;color:var(--text-dim);">@${escapeHtml(u.username || '')}</div>` : `<div style="font-size:13px;color:var(--text-dim);">${escapeHtml(u.email || '')}</div>`}
<input type="file" id="p-photo-input" accept="image/jpeg,image/png,image/webp" style="display:none;">
<button type="button" class="btn-b ghost tiny" id="p-upload" style="margin-top:8px;">📷 Загрузить фото</button>
</div>
</div>
<!-- поля -->
<div class="row-list" style="margin-top:14px;">
<label class="field-b">
<span>Имя</span>
<input class="ctrl" id="p-name" value="${escapeHtml(u.name || '')}" placeholder="Как тебя зовут">
<div class="hint mute" id="h-name"></div>
</label>
<label class="field-b">
<span>Город</span>
<input class="ctrl" id="p-city" placeholder="Москва" value="${escapeHtml(u.city || '')}">
</label>
<label class="field-b">
<span>О себе</span>
<textarea class="ctrl" id="p-bio" rows="3" placeholder="Пару слов о себе" style="resize:none;">${escapeHtml(u.bio || '')}</textarea>
<div class="hint mute" id="h-bio" style="text-align:right;font-size:11px;"></div>
</label>
<label class="field-b">
<span>Пол</span>
<select class="ctrl" id="p-gender" style="font:15px/1.2 inherit;padding:13px 14px;">
<option value="">Не указан</option>
<option value="m" ${u.gender === 'm' ? 'selected' : ''}>Мужской</option>
<option value="f" ${u.gender === 'f' ? 'selected' : ''}>Женский</option>
<option value="o" ${u.gender === 'o' ? 'selected' : ''}>Другое</option>
</select>
</label>
</div>
<!-- чипы (drink/activity/purpose) -->
<div class="row-list" style="margin-top:14px;">
<div class="card-b">
<div style="font-size:12px;font-weight:600;color:var(--text-dim);margin-bottom:10px;">🍻 Что пью</div>
<div class="chip-list" id="p-drinks"></div>
</div>
<div class="card-b">
<div style="font-size:12px;font-weight:600;color:var(--text-dim);margin-bottom:10px;">🎯 Чем занимаюсь</div>
<div class="chip-list" id="p-activities"></div>
</div>
<div class="card-b">
<div style="font-size:12px;font-weight:600;color:var(--text-dim);margin-bottom:10px;">💡 Цели</div>
<div class="chip-list" id="p-purposes"></div>
</div>
</div>
<button class="btn-b" id="p-save" style="margin-top:14px;">Сохранить</button>
</div>
`;
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 = `
<div class="view">
<h1 style="margin:0 0 14px;font-size:22px;">👤 ${escapeHtml(this.userName)}</h1>
<div id="up-content">${'<div class="empty-b">⏳ Загружаю…</div>'}</div>
</div>
`;
try {
const [user, stats] = await Promise.all([
API.UserApi.get(this.userId),
API.ReviewApi.stats(this.userId).catch(() => ({})),
]);
console.log('[UserProfile] loaded:', u && u.id, '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 = `
<!-- Аватар и имя -->
<div class="card-b" style="display:flex;align-items:center;gap:14px;">
<div class="avatar-b lg" style="${u.photo ? `background-image:url(${u.photo});background-size:cover;color:transparent;` : ''}">${u.photo ? '' : init}</div>
<div style="flex:1;">
<div style="font-weight:700;font-size:18px;">${escapeHtml(u.name || '')}</div>
${u.city ? `<div style="font-size:13px;color:var(--text-dim);">📍 ${escapeHtml(u.city)}</div>` : ''}
${u.age ? `<div style="font-size:13px;color:var(--text-dim);">🎂 ${u.age} лет</div>` : ''}
${reviewsCount > 0 ? `<div style="font-size:13px;color:var(--warn);margin-top:4px;">★ ${rating} · ${reviewsCount} ${pluralRu(reviewsCount, 'отзыв', 'отзыва', 'отзывов')}</div>` : ''}
</div>
</div>
<!-- О себе -->
${u.bio ? `
<div class="card-b" style="margin-top:12px;">
<div style="font-size:12px;font-weight:600;color:var(--text-dim);margin-bottom:6px;">О себе</div>
<div style="font-size:14px;line-height:1.4;">${escapeHtml(u.bio)}</div>
</div>
` : ''}
<!-- Действия -->
<div class="row-list" style="margin-top:14px;">
<button class="btn-b" id="up-message">💬 Написать</button>
${reviewsCount > 0 ? `<button class="btn-b ghost" id="up-reviews">⭐ Все отзывы</button>` : ''}
</div>
`;
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 = `<div class="empty-b">⚠️ Не удалось загрузить</div>`;
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;
}