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 = `
-
-
-
${escapeHtml(title)}
-
${escapeHtml(text || 'Не заполнено')}
-
-
-
`;
- 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 = `
-
+
`;
+
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'}`;
});