diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js index 6302bc6e..e104a9fc 100644 --- a/shine-UI/js/pages/profile-view.js +++ b/shine-UI/js/pages/profile-view.js @@ -1,12 +1,9 @@ import { profile } from '../mock-data.js'; -import { authService, state } from '../state.js'; -import { - loadProfileSnapshot, -} from '../services/user-profile-params.js'; +import { state } from '../state.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'; +import { loadUserProfileCard } from '../services/user-connections.js'; export const pageMeta = { id: 'profile-view', title: 'Профиль' }; @@ -19,172 +16,194 @@ function escapeHtml(text) { .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 metricHtml({ kind, label, value, glow = false, positionClass = '' }) { + const numericValue = Number(value || 0); + return ` + `; } -function openTextModal(title, text) { - const root = document.getElementById('modal-root'); - if (!root) return; - root.innerHTML = ` - `; - 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 spiritualPathDetailHtml(card) { + const value = String(card?.spiritualPath || '').trim(); + return `
${escapeHtml(value || 'Не заполнено')}
`; } -function statusBadges(accountRole, shineStatus) { - const role = accountRole === 'primary' ? 'Основной аккаунт' : accountRole === 'non_voting' ? 'Голос не учитывать' : ''; - const shine = shineStatus === 'shining' ? 'Сияющий' : ''; - return `
- ${role ? `${escapeHtml(role)}` : ''} - ${shine ? 'Сияющий' : ''} - ${shineStatus === 'not_interested' ? 'Сияние неинтересно' : ''} -
`; -} +function contactsDetailHtml(card) { + const rows = [ + ['Ссылки', card?.web], + ['Телефон', card?.phone], + ['Адрес', card?.address], + ].filter(([, value]) => String(value || '').trim()); -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], - ]; + if (!rows.length) { + return '
Не заполнено
'; + } + return rows.map(([label, value]) => ` +
+ ${escapeHtml(label)} + ${escapeHtml(value)} +
`).join(''); } export function render({ navigate, chrome }) { const login = String(state.session.login || profile.login || '').trim(); const screen = document.createElement('section'); - screen.className = 'stack profile-screen'; + screen.className = 'stack user-profile-screen'; const topbar = document.createElement('header'); - topbar.className = 'page-header app-topbar-shell app-topbar-shell--profile'; + topbar.className = 'page-header app-topbar-shell app-topbar-shell--profile user-profile-header'; topbar.innerHTML = ` -

Профиль

+

${escapeHtml(login || 'Профиль')}

`; + const menuButton = topbar.querySelector('.profile-head-menu-btn'); menuButton?.append(createOverflowDots()); const profileMenu = createDropdownMenu({ anchorEl: menuButton, className: 'profile-head-menu', - minWidth: 230, + minWidth: 250, 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(`SHiNE/${encodeURIComponent(login)}/list/primary_given`) }, + { label: 'Подтверждённые сияющие', action: () => navigate(`SHiNE/${encodeURIComponent(login)}/list/shine_given`) }, { label: 'Сменить профиль', action: () => navigate('profiles-view') }, ], }); chrome?.setTopbar(topbar); const status = document.createElement('div'); - status.className = 'status-line'; + status.className = 'status-line user-profile-status'; status.textContent = 'Загрузка профиля...'; + const body = document.createElement('div'); - body.className = 'stack'; + body.className = 'user-profile-body'; screen.append(status, body); - let current = null; + let card = 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), - }; + if (!card) return; + const stats = card.stats || {}; + const official = card.accountRole === 'primary'; + const shining = card.shineStatus === 'shining'; + const fullName = [card.firstName, card.lastName] + .map((value) => String(value || '').trim()) + .filter(Boolean) + .join(' '); + const about = String(card.about || '').trim(); - 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 = `
${escapeHtml(displayName)}
${escapeHtml(login)}
`; - identity.append(identityText); - body.append(identity); + const title = topbar.querySelector('.page-title'); + if (title) title.textContent = card.login || login || 'Профиль'; - const badges = document.createElement('div'); - badges.innerHTML = statusBadges(String(snapshot?.accountRole || '').trim().toLowerCase(), String(snapshot?.shineStatus || '').trim().toLowerCase()); - body.append(...badges.children); + body.innerHTML = ` + ${fullName ? `
${escapeHtml(fullName)}
` : ''} - 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); - } +
+ ${metricHtml({ kind: 'primary_received', label: 'Официальный', value: stats.primaryReceivedCount, glow: official, positionClass: 'is-top-left' })} + ${metricHtml({ kind: 'shine_received', label: 'Сияющий', value: stats.shineReceivedCount, glow: shining, positionClass: 'is-top-right' })} +
+ ${metricHtml({ kind: 'friends', label: 'Друзья', value: stats.friendsCount, positionClass: 'is-bottom-left' })} + ${metricHtml({ kind: 'close_friends', label: 'Близкие друзья', value: stats.closeFriendsCount, positionClass: 'is-bottom-right' })} +
- 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 = `${Number(value || 0)}${escapeHtml(label)}`; - statsGrid.append(button); - }); - body.append(statsGrid); + ${about ? `
${escapeHtml(about)}
` : ''} - const detailRow = document.createElement('div'); - detailRow.className = 'row wrap-row'; - detailRow.innerHTML = ''; - body.append(detailRow); +
+ ${metricHtml({ kind: 'channels_following', label: 'Подписки', value: stats.followingChannelsCount, positionClass: 'is-channel-metric' })} + ${metricHtml({ kind: 'channels_owned', label: 'Каналы', value: stats.ownedPublicChannelsCount, positionClass: 'is-channel-metric' })} +
+ +
+
+ + + +
+
+ + + `; + + const avatarSlot = body.querySelector('.user-profile-avatar-slot'); + avatarSlot?.append(renderUserAvatar({ + login: card.login, + firstName: card.firstName, + lastName: card.lastName, + avatar: card.avatar, + size: 'xl', + className: 'user-profile-hero-avatar', + glow: shining, + })); + + status.textContent = ''; } 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)}`); + if (!card) return; + + const listButton = event.target.closest('[data-profile-list]'); + if (listButton) { + const kind = listButton.dataset.profileList; + if (kind) navigate(`SHiNE/${encodeURIComponent(card.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') || 'Не заполнено'); + + const actionButton = event.target.closest('[data-self-profile-action]'); + if (actionButton) { + const action = actionButton.dataset.selfProfileAction; + if (action === 'edit') navigate('profile-edit-view'); + if (action === 'wallet') navigate('wallet-view'); + if (action === 'settings') navigate('settings-view'); + return; } - if (el.dataset.profileDetail === 'spiritual') openTextModal('Духовный путь', fields.spiritual_path); + + const detailButton = event.target.closest('[data-profile-detail]'); + if (!detailButton) return; + const detailKind = detailButton.dataset.profileDetail; + const detailPanel = body.querySelector('#profile-view-detail-panel'); + if (!detailPanel) return; + + body.querySelectorAll('[data-profile-detail]').forEach((button) => { + const active = button === detailButton; + button.classList.toggle('is-active', active); + button.setAttribute('aria-pressed', active ? 'true' : 'false'); + }); + + if (detailKind === 'spiritual-path') { + detailPanel.innerHTML = spiritualPathDetailHtml(card); + } else if (detailKind === 'contacts') { + detailPanel.innerHTML = contactsDetailHtml(card); + } else { + return; + } + + detailPanel.hidden = false; + detailPanel.dataset.activeDetail = detailKind; }); async function refresh() { @@ -192,14 +211,12 @@ export function render({ navigate, chrome }) { status.textContent = 'Локальный тестовый режим.'; return; } - const [snapshot, user] = await Promise.all([loadProfileSnapshot(login), authService.getUser(login)]); - current = { snapshot, user }; + card = await loadUserProfileCard(login); renderProfile(); - status.textContent = ''; } refresh().catch((error) => { - status.className = 'status-line is-unavailable'; + status.className = 'status-line user-profile-status is-unavailable'; status.textContent = `Ошибка: ${error?.message || 'unknown'}`; });