import { profile } from '../mock-data.js'; import { authService, state } from '../state.js'; import { PROFILE_GENDER_FEMALE, PROFILE_GENDER_MALE, loadProfileSnapshot, } from '../services/user-profile-params.js'; import { buildIdentityLines } from '../services/user-connections.js'; import { renderUserAvatar } from '../components/avatar-image.js'; import { createOverflowDots } from '../components/overflow-dots.js'; import { createDropdownMenu } from '../components/dropdown-menu.js'; export const pageMeta = { id: 'profile-view', title: 'Профиль' }; function toggleText(enabled) { return enabled ? 'Yes' : 'No'; } function genderLabel(value) { if (value === PROFILE_GENDER_MALE) return 'Мужской'; if (value === PROFILE_GENDER_FEMALE) return 'Женский'; return 'Не указан'; } function escapeHtml(text) { return String(text || '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function renderProfileInfoText(text) { const [intro, details] = String(text || '').split(/\n\n/, 2); if (!details) { return `

${escapeHtml(intro)}

`; } const [sectionTitle, ...items] = details.split('\n').filter(Boolean); return `

${escapeHtml(intro)}

${escapeHtml(sectionTitle)}

    ${items.map((item) => `
  1. ${escapeHtml(item.replace(/^\d+\)\s*/, ''))}
  2. `).join('')}
`; } function openProfileInfoModal({ title, text }) { const root = document.getElementById('modal-root'); if (!root) return; root.innerHTML = ` `; const close = () => { root.innerHTML = ''; }; root.querySelector('#profile-info-close')?.addEventListener('click', close); root.querySelector('#profile-info-close-icon')?.addEventListener('click', close); root.querySelector('#profile-info-modal')?.addEventListener('click', (event) => { if (event.target?.id === 'profile-info-modal') close(); }); } function officialInfoText() { return 'Можно создавать несколько альтернативных или анонимных каналов. ' + 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.'; } function shineInfoText() { return 'Сияющие — это те, от кого идёт внутреннее сияние на тонком плане.\n\n' + 'Пять принципов сияющих:\n' + '1) сияющие не обманывают;\n' + '2) сияющие чувствуют, что человек — это не только физическое тело, а нечто большее;\n' + '3) сияющие развиваются и в духовной, и в материальной плоскости;\n' + '4) у сияющих есть близкие друзья, с которыми им по-настоящему хорошо;\n' + '5) сияющие заботятся о мире: о людях, гармонии и общем благе.'; } export function render({ navigate, chrome }) { const login = state.session.login || profile.login; 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 = `

Профиль

`; const profileMenuButton = topbar.querySelector('.profile-head-menu-btn'); profileMenuButton?.append(createOverflowDots()); const profileMenu = createDropdownMenu({ anchorEl: profileMenuButton, 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') }, ], }); chrome?.setTopbar(topbar); const card = document.createElement('div'); card.className = 'card stack profile-main-card'; const topRow = document.createElement('div'); topRow.className = 'row'; topRow.innerHTML = `
`; const badgesRow = document.createElement('div'); badgesRow.className = 'row'; badgesRow.innerHTML = ` `; const listWrap = document.createElement('div'); listWrap.className = 'stack profile-param-list'; const accountRoleBtn = badgesRow.querySelector('[data-status="account_role"]'); const shineBtn = badgesRow.querySelector('[data-status="shine"]'); const identityEl = topRow.querySelector('[data-profile-identity="true"]'); const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]'); let currentFields = []; let currentAccountRole = ''; let currentShineStatus = ''; let currentGender = 'unknown'; let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; let currentStats = { ownedPublicChannelsCount: 0, followingUsersCount: 0, followingChannelsCount: 0, closeFriendsCount: 0, }; function syncIdentity() { if (!identityEl) return; const firstName = currentFields.find((field) => field.key === 'first_name')?.value || ''; const lastName = currentFields.find((field) => field.key === 'last_name')?.value || ''; const lines = buildIdentityLines({ login, firstName, lastName }); identityEl.innerHTML = lines.map((line, idx) => ( `
${escapeHtml(line)}
` )).join(''); } function updateAvatarUi() { if (!(avatarSlotEl instanceof HTMLElement)) return; const firstName = String(currentFields.find((field) => field.key === 'first_name')?.value || '').trim(); const lastName = String(currentFields.find((field) => field.key === 'last_name')?.value || '').trim(); avatarSlotEl.innerHTML = ''; avatarSlotEl.append(renderUserAvatar({ login, firstName, lastName, avatar: currentAvatar?.txId ? { ar: currentAvatar.txId, sha256Hex: String(currentAvatar?.sha256Hex || '').trim().toLowerCase() } : null, size: 'xl', className: 'profile-avatar', })); } function updateStatusesUi() { if (accountRoleBtn) { const label = currentAccountRole === 'primary' ? 'Основной аккаунт' : currentAccountRole === 'non_voting' ? 'Не учитывать мой голос' : 'Не указано'; accountRoleBtn.textContent = `Аккаунт: ${label}`; accountRoleBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested'); accountRoleBtn.classList.add(currentAccountRole === 'primary' ? 'is-yes-official' : 'is-no'); } if (shineBtn) { const label = currentShineStatus === 'shining' ? 'Сияющий' : currentShineStatus === 'not_interested' ? 'Сияние неинтересно' : currentShineStatus === 'unknown' ? 'Неизвестно' : 'Не указано'; shineBtn.textContent = `Сияние: ${label}`; shineBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested'); shineBtn.classList.add(currentShineStatus === 'shining' ? 'is-yes-shine' : currentShineStatus === 'not_interested' ? 'is-not-interested' : 'is-no'); } } function renderStats() { const stats = [ { label: 'Собственные публичные каналы', value: currentStats.ownedPublicChannelsCount }, { label: 'Подписки на пользователей', value: currentStats.followingUsersCount }, { label: 'Подписки на каналы', value: currentStats.followingChannelsCount }, { label: 'Близкие друзья', value: currentStats.closeFriendsCount }, ]; stats.forEach((stat) => { const row = document.createElement('div'); row.className = 'card profile-param-item row'; row.innerHTML = `
${escapeHtml(stat.label)}: ${escapeHtml(String(Number(stat.value || 0)))}
`; listWrap.append(row); }); } accountRoleBtn?.classList.add('profile-badge-trigger'); shineBtn?.classList.add('profile-badge-trigger'); accountRoleBtn?.addEventListener('click', () => { openProfileInfoModal({ title: 'Основной аккаунт', text: officialInfoText(), }); }); shineBtn?.addEventListener('click', () => { openProfileInfoModal({ title: 'Справка о сияющих', text: shineInfoText(), }); }); function renderFields(fields) { listWrap.innerHTML = ''; renderStats(); fields.forEach((field) => { const row = document.createElement('div'); row.className = 'card profile-param-item row'; const value = String(field.value || '').trim() || 'не заполнено'; row.innerHTML = `
${field.label}: ${escapeHtml(value)}
`; listWrap.append(row); if (field.key === 'last_name') { const genderRow = document.createElement('div'); genderRow.className = 'card profile-param-item row'; genderRow.innerHTML = `
Пол: ${escapeHtml(genderLabel(currentGender))}
`; listWrap.append(genderRow); } }); } async function refreshProfileSnapshot() { if (state.session.isLocalDemo) { currentFields = [ { key: 'first_name', label: 'Имя', value: 'Тестовый' }, { key: 'last_name', label: 'Фамилия', value: 'Пользователь' }, { key: 'address', label: 'Адрес', value: 'Локальный режим' }, { key: 'website', label: 'Веб', value: '127.0.0.1' }, { key: 'phone', label: 'Телефон', value: profile.phone }, ]; currentAccountRole = ''; currentShineStatus = ''; currentGender = 'unknown'; currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; currentStats = { ownedPublicChannelsCount: 0, followingUsersCount: 0, followingChannelsCount: 0, closeFriendsCount: 0, }; syncIdentity(); updateAvatarUi(); updateStatusesUi(); renderFields(currentFields); return; } try { const [snapshot, user] = await Promise.all([ loadProfileSnapshot(login), authService.getUser(login).catch(() => ({})), ]); currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : []; currentAccountRole = snapshot.accountRole || ''; currentShineStatus = snapshot.shineStatus || ''; currentGender = snapshot.gender || 'unknown'; currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; currentStats = { ownedPublicChannelsCount: Number(user?.ownedPublicChannelsCount || 0), followingUsersCount: Number(user?.followingUsersCount || 0), followingChannelsCount: Number(user?.followingChannelsCount || 0), closeFriendsCount: Number(user?.closeFriendsCount || 0), }; syncIdentity(); updateAvatarUi(); updateStatusesUi(); renderFields(currentFields); } catch (error) { // ignore status row in profile-view } } card.append(topRow, badgesRow, listWrap); screen.append(card); updateAvatarUi(); refreshProfileSnapshot(); screen.cleanup = () => profileMenu.destroy(); return screen; }