Fix: UserProfileView для тапа на маркер человека

Проблема: app.js вызывал UserProfileView.render при showView('user-profile'),
но view был не создан. ReferenceError.

Создал UserProfileView в profile.js:
- Читает /users/:id + /users/:id/stats
- Card с avatar-b.lg, name, city, ★ rating (если есть отзывы)
- bio
- Кнопки: 'Написать' (ensureChat → openChat) + 'Все отзывы' (UserReviewsView)
- Error/toast на любой сбой

API: добавил UserApi.get|reviews|stats namespace.
This commit is contained in:
root 2026-08-21 11:59:09 +00:00
parent c5f8ac67ad
commit f6fa3fedab
3 changed files with 106 additions and 12 deletions

View File

@ -8,7 +8,7 @@
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<link rel="stylesheet" href="/css/app.css?v=24">
<link rel="stylesheet" href="/css/app.css?v=25">
</head>
<body>
<button id="mode-toggle" title="Переключить режим">📱 Мобильный</button>
@ -34,15 +34,15 @@
</button>
</nav>
</div>
<script src="/js/tg.js?v=24"></script>
<script src="/js/api.js?v=24"></script>
<script src="/js/views/map.js?v=24"></script>
<script src="/js/views/chats.js?v=24"></script>
<script src="/js/views/chat.js?v=24"></script>
<script src="/js/views/profile.js?v=24"></script>
<script src="/js/views/settings.js?v=24"></script>
<script src="/js/views/onboarding.js?v=24"></script>
<script src="/js/views/review.js?v=24"></script>
<script src="/js/app.js?v=24"></script>
<script src="/js/tg.js?v=25"></script>
<script src="/js/api.js?v=25"></script>
<script src="/js/views/map.js?v=25"></script>
<script src="/js/views/chats.js?v=25"></script>
<script src="/js/views/chat.js?v=25"></script>
<script src="/js/views/profile.js?v=25"></script>
<script src="/js/views/settings.js?v=25"></script>
<script src="/js/views/onboarding.js?v=25"></script>
<script src="/js/views/review.js?v=25"></script>
<script src="/js/app.js?v=25"></script>
</body>
</html>

View File

@ -103,6 +103,13 @@ const Auth = {
getUser: function (id) { return apiGet('/api/v1/users/' + id); },
};
// === API namespaces ===
const UserApi = {
get: function (userId) { return apiGet('/api/v1/users/' + userId); },
reviews: function (userId) { return apiGet('/api/v1/users/' + userId + '/reviews'); },
stats: function (userId) { return apiGet('/api/v1/users/' + userId + '/stats'); },
};
const ChatApi = {
ensureChat: function (otherId) { return apiPost('/api/v1/chats', { other_id: otherId }); },
listChats: function () { return apiGet('/api/v1/chats'); },

View File

@ -198,4 +198,91 @@ const ProfileView = {
},
};
window.ProfileView = ProfileView;
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 = `
<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(() => ({})),
]);
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_url ? `background-image:url(${u.photo_url});background-size:cover;color:transparent;` : ''}">${u.photo_url ? '' : 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>` : ''}
${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) {
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;
}