SHA256
209 lines
9.2 KiB
JavaScript
209 lines
9.2 KiB
JavaScript
import { profile } from '../mock-data.js';
|
|
import { authService, state } from '../state.js';
|
|
import {
|
|
loadProfileSnapshot,
|
|
} from '../services/user-profile-params.js';
|
|
import { renderUserAvatar } from '../components/avatar-image.js';
|
|
import { createOverflowDots } from '../components/overflow-dots.js';
|
|
import { createDropdownMenu } from '../components/dropdown-menu.js';
|
|
import { userDisplayName } from '../services/user-display.js';
|
|
|
|
export const pageMeta = { id: 'profile-view', title: 'Профиль' };
|
|
|
|
function escapeHtml(text) {
|
|
return String(text || '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function fieldMap(snapshot) {
|
|
const out = {};
|
|
(Array.isArray(snapshot?.fields) ? snapshot.fields : []).forEach((field) => {
|
|
out[String(field?.key || '').trim()] = String(field?.value || '').trim();
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function openTextModal(title, text) {
|
|
const root = document.getElementById('modal-root');
|
|
if (!root) return;
|
|
root.innerHTML = `
|
|
<div class="modal" id="profile-text-modal">
|
|
<div class="modal-card stack">
|
|
<h3>${escapeHtml(title)}</h3>
|
|
<div style="white-space:pre-wrap;line-height:1.5">${escapeHtml(text || 'Не заполнено')}</div>
|
|
<button class="secondary-btn" id="profile-text-close">Закрыть</button>
|
|
</div>
|
|
</div>`;
|
|
const close = () => { root.innerHTML = ''; };
|
|
root.querySelector('#profile-text-close')?.addEventListener('click', close);
|
|
root.querySelector('#profile-text-modal')?.addEventListener('click', (event) => {
|
|
if (event.target?.id === 'profile-text-modal') close();
|
|
});
|
|
}
|
|
|
|
function statusBadges(accountRole, shineStatus) {
|
|
const role = accountRole === 'primary' ? 'Основной аккаунт' : accountRole === 'non_voting' ? 'Голос не учитывать' : '';
|
|
const shine = shineStatus === 'shining' ? 'Сияющий' : '';
|
|
return `<div class="row wrap-row">
|
|
${role ? `<span class="badge">${escapeHtml(role)}</span>` : ''}
|
|
${shine ? '<span class="badge is-yes-shine">Сияющий</span>' : ''}
|
|
${shineStatus === 'not_interested' ? '<span class="profile-shine-not-interested" title="Сияние неинтересно"><img src="/assets/shine-status-not-interested.svg" alt="Сияние неинтересно"></span>' : ''}
|
|
</div>`;
|
|
}
|
|
|
|
function statsRows(stats = {}) {
|
|
return [
|
|
['friends', 'Друзья', stats.friendsCount],
|
|
['close_friends', 'Близкие друзья', stats.closeFriendsCount],
|
|
['primary_received', 'Подтвердили основной аккаунт', stats.primaryReceivedCount],
|
|
['primary_given', 'Подтверждённые аккаунты', stats.primaryGivenCount],
|
|
['shine_received', 'Считают сияющим', stats.shineReceivedCount],
|
|
['shine_given', 'Подтверждённые сияющие', stats.shineGivenCount],
|
|
['channels_following', 'Подписки на каналы', stats.followingChannelsCount],
|
|
['channels_owned', 'Каналы', stats.ownedPublicChannelsCount],
|
|
];
|
|
}
|
|
|
|
export function render({ navigate, chrome }) {
|
|
const login = String(state.session.login || profile.login || '').trim();
|
|
const screen = document.createElement('section');
|
|
screen.className = 'stack profile-screen';
|
|
|
|
const topbar = document.createElement('header');
|
|
topbar.className = 'page-header app-topbar-shell app-topbar-shell--profile';
|
|
topbar.innerHTML = `
|
|
<div class="header-left" aria-hidden="true"></div>
|
|
<div class="header-center"><h1 class="page-title">Профиль</h1></div>
|
|
<div class="header-actions profile-head-menu-wrap dm-head-menu-wrap">
|
|
<button type="button" class="dm-head-menu-btn profile-head-menu-btn" aria-label="Меню профиля" aria-haspopup="menu" aria-expanded="false"></button>
|
|
</div>`;
|
|
const menuButton = topbar.querySelector('.profile-head-menu-btn');
|
|
menuButton?.append(createOverflowDots());
|
|
const profileMenu = createDropdownMenu({
|
|
anchorEl: menuButton,
|
|
className: 'profile-head-menu',
|
|
minWidth: 230,
|
|
items: [
|
|
{ label: 'Редактировать профиль', iconSrc: '/assets/profile-icon-profile.svg', action: () => navigate('profile-edit-view') },
|
|
{ label: 'Кошелёк', iconSrc: '/assets/profile-icon-wallet.svg', action: () => navigate('wallet-view') },
|
|
{ label: 'Настройки', iconSrc: '/assets/profile-icon-settings.svg', action: () => navigate('settings-view') },
|
|
{ label: 'Сменить профиль', action: () => navigate('profiles-view') },
|
|
],
|
|
});
|
|
chrome?.setTopbar(topbar);
|
|
|
|
const status = document.createElement('div');
|
|
status.className = 'status-line';
|
|
status.textContent = 'Загрузка профиля...';
|
|
const body = document.createElement('div');
|
|
body.className = 'stack';
|
|
screen.append(status, body);
|
|
|
|
let current = null;
|
|
|
|
function renderProfile() {
|
|
if (!current) return;
|
|
const { snapshot, user } = current;
|
|
const fields = fieldMap(snapshot);
|
|
const firstName = fields.first_name || '';
|
|
const lastName = fields.last_name || '';
|
|
const displayName = userDisplayName({ login, firstName, lastName });
|
|
const avatar = snapshot?.avatar?.txId
|
|
? { ar: String(snapshot.avatar.txId).trim(), sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase() }
|
|
: null;
|
|
const stats = {
|
|
ownedPublicChannelsCount: Number(user?.ownedPublicChannelsCount || 0),
|
|
followingChannelsCount: Number(user?.followingChannelsCount || 0),
|
|
friendsCount: Number(user?.friendsCount || 0),
|
|
closeFriendsCount: Number(user?.closeFriendsCount || 0),
|
|
primaryReceivedCount: Number(user?.primaryConfirmationsReceivedCount || 0),
|
|
primaryGivenCount: Number(user?.primaryConfirmationsGivenCount || 0),
|
|
shineReceivedCount: Number(user?.shineConfirmationsReceivedCount || 0),
|
|
shineGivenCount: Number(user?.shineConfirmationsGivenCount || 0),
|
|
};
|
|
|
|
body.innerHTML = '';
|
|
const identity = document.createElement('div');
|
|
identity.className = 'card row';
|
|
identity.style.gap = '12px';
|
|
identity.style.alignItems = 'center';
|
|
identity.append(renderUserAvatar({ login, firstName, lastName, avatar, size: 'xl', className: 'profile-avatar' }));
|
|
const identityText = document.createElement('div');
|
|
identityText.innerHTML = `<div class="profile-identity-line">${escapeHtml(displayName)}</div><div class="profile-identity-login">${escapeHtml(login)}</div>`;
|
|
identity.append(identityText);
|
|
body.append(identity);
|
|
|
|
const badges = document.createElement('div');
|
|
badges.innerHTML = statusBadges(String(snapshot?.accountRole || '').trim().toLowerCase(), String(snapshot?.shineStatus || '').trim().toLowerCase());
|
|
body.append(...badges.children);
|
|
|
|
if (fields.about) {
|
|
const about = document.createElement('div');
|
|
about.className = 'card profile-about';
|
|
about.style.whiteSpace = 'pre-wrap';
|
|
about.textContent = fields.about;
|
|
body.append(about);
|
|
}
|
|
|
|
const statsGrid = document.createElement('div');
|
|
statsGrid.className = 'profile-stats-grid';
|
|
statsRows(stats).forEach(([kind, label, value]) => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'card profile-stat-card';
|
|
button.dataset.profileList = kind;
|
|
button.innerHTML = `<b>${Number(value || 0)}</b><span>${escapeHtml(label)}</span>`;
|
|
statsGrid.append(button);
|
|
});
|
|
body.append(statsGrid);
|
|
|
|
const detailRow = document.createElement('div');
|
|
detailRow.className = 'row wrap-row';
|
|
detailRow.innerHTML = '<button class="secondary-btn" data-profile-detail="contacts">Контакты</button><button class="secondary-btn" data-profile-detail="spiritual">Духовный путь</button>';
|
|
body.append(detailRow);
|
|
}
|
|
|
|
body.addEventListener('click', (event) => {
|
|
if (!current) return;
|
|
const el = event.target.closest('[data-profile-list],[data-profile-detail]');
|
|
if (!el) return;
|
|
const kind = el.dataset.profileList;
|
|
if (kind) {
|
|
navigate(`SHiNE/${encodeURIComponent(login)}/list/${encodeURIComponent(kind)}`);
|
|
return;
|
|
}
|
|
const fields = fieldMap(current.snapshot);
|
|
if (el.dataset.profileDetail === 'contacts') {
|
|
openTextModal('Контакты', [
|
|
fields.web ? `Links: ${fields.web}` : '',
|
|
fields.phone ? `Телефон: ${fields.phone}` : '',
|
|
fields.address ? `Адрес: ${fields.address}` : '',
|
|
].filter(Boolean).join('\n') || 'Не заполнено');
|
|
}
|
|
if (el.dataset.profileDetail === 'spiritual') openTextModal('Духовный путь', fields.spiritual_path);
|
|
});
|
|
|
|
async function refresh() {
|
|
if (state.session.isLocalDemo) {
|
|
status.textContent = 'Локальный тестовый режим.';
|
|
return;
|
|
}
|
|
const [snapshot, user] = await Promise.all([loadProfileSnapshot(login), authService.getUser(login)]);
|
|
current = { snapshot, user };
|
|
renderProfile();
|
|
status.textContent = '';
|
|
}
|
|
|
|
refresh().catch((error) => {
|
|
status.className = 'status-line is-unavailable';
|
|
status.textContent = `Ошибка: ${error?.message || 'unknown'}`;
|
|
});
|
|
|
|
screen.cleanup = () => profileMenu.destroy();
|
|
return screen;
|
|
}
|