Files
SHiNE-server/shine-UI/js/pages/user-profile-view.js
T

372 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.js';
import {
buildIdentityLines,
loadRelationsForPair,
loadUserProfileCard,
} from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { makeProfileLinksRoute } from '../services/shine-routes.js';
import { navigateBack } from '../router.js';
export const pageMeta = { id: 'user', title: 'Чужой профиль' };
function escapeHtml(text) {
return String(text || '')
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function openProfileInfoModal({ title, text }) {
const root = document.getElementById('modal-root');
if (!root) return;
root.innerHTML = `
<div class="modal" id="profile-info-modal">
<div class="modal-card stack">
<h3 class="modal-title">${escapeHtml(title)}</h3>
<p class="meta-muted" style="white-space: pre-wrap; line-height: 1.45;">${escapeHtml(text)}</p>
<button class="secondary-btn" type="button" id="profile-info-close">Закрыть</button>
</div>
</div>
`;
const close = () => { root.innerHTML = ''; };
root.querySelector('#profile-info-close')?.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) сияющие заботятся о мире: о людях, гармонии и общем благе.';
}
function genderText(value) {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'male') return 'Мужской';
if (normalized === 'female') return 'Женский';
return 'Не указан';
}
function relationButtonLabel(kind, flags) {
if (kind === 'contact') return flags.outContact ? 'Убрать из контактов' : 'Добавить в контакты';
if (kind === 'friend') return flags.outFriend ? 'Убрать из друзей' : 'Добавить в друзья';
if (kind === 'close_friend') return flags.outCloseFriend ? 'Убрать из близких друзей' : 'Добавить в близкие друзья';
if (kind === 'official_account') return flags.outOfficialAccount ? 'Снять подтверждение' : 'Подтвердить аккаунт';
if (kind === 'shine_confirmed') return flags.outShineConfirmed ? 'Снять подтверждение' : 'Подтвердить сияющего';
return '';
}
function relationNextState(kind, flags) {
if (kind === 'contact') return !flags.outContact;
if (kind === 'friend') return !flags.outFriend;
if (kind === 'close_friend') return !flags.outCloseFriend;
if (kind === 'official_account') return !flags.outOfficialAccount;
if (kind === 'shine_confirmed') return !flags.outShineConfirmed;
return false;
}
function relationConfirmLabel(kind) {
if (kind === 'contact') return 'контакт';
if (kind === 'friend') return 'статус друга';
if (kind === 'close_friend') return 'статус близкого друга';
if (kind === 'official_account') return 'подтверждение официального аккаунта';
if (kind === 'shine_confirmed') return 'подтверждение сияющего';
return 'связь';
}
function relationStateText(kind, flags) {
if (kind === 'contact') {
if (flags.outContact && flags.inContact) return 'Вы обменялись контактами.';
if (flags.outContact) return 'Вы добавили этот профиль в контакты.';
if (flags.inContact) return 'Этот профиль добавил вас в контакты.';
return '';
}
if (kind === 'friend') {
if (flags.outFriend && flags.inFriend) return 'Вы взаимно считаете друг друга друзьями.';
if (flags.outFriend) return 'Вы считаете этого человека другом.';
if (flags.inFriend) return 'Этот человек считает вас другом.';
return '';
}
if (kind === 'close_friend') {
if (flags.outCloseFriend && flags.inCloseFriend) return 'Вы взаимно близкие друзья.';
if (flags.outCloseFriend) return 'Вы считаете этого человека близким другом.';
if (flags.inCloseFriend) return 'Этот человек считает вас близким другом.';
return '';
}
if (kind === 'official_account') {
return flags.outOfficialAccount
? 'Вы подтверждаете, что это действительно официальный аккаунт этого человека.'
: '';
}
if (kind === 'shine_confirmed') {
return flags.outShineConfirmed
? 'Вы подтверждаете, что этот человек сияющий.'
: '';
}
return '';
}
function renderRelations(flags) {
const rows = [
{ kind: 'contact', text: relationStateText('contact', flags), button: relationButtonLabel('contact', flags) },
{ kind: 'friend', text: relationStateText('friend', flags), button: relationButtonLabel('friend', flags) },
{ kind: 'close_friend', text: relationStateText('close_friend', flags), button: relationButtonLabel('close_friend', flags) },
{ kind: 'official_account', text: relationStateText('official_account', flags), button: relationButtonLabel('official_account', flags) },
{ kind: 'shine_confirmed', text: relationStateText('shine_confirmed', flags), button: relationButtonLabel('shine_confirmed', flags) },
];
return `
<div class="card stack user-relations-list" data-profile-relations="true">
${rows.map((row) => `
<div class="user-rel-row ${row.text ? '' : 'is-empty'}">
<span class="user-rel-text">${escapeHtml(row.text)}</span>
<button class="ghost-btn user-rel-action" type="button" data-relation-action="${row.kind}">${escapeHtml(row.button)}</button>
</div>
`).join('')}
</div>
`;
}
function renderIdentity(card) {
const lines = buildIdentityLines({
login: card.login,
firstName: card.firstName,
lastName: card.lastName,
});
const row = document.createElement('div');
row.className = 'row';
row.style.gap = '12px';
row.style.alignItems = 'center';
row.append(renderUserAvatar({
login: card.login,
firstName: card.firstName,
lastName: card.lastName,
avatar: card.avatar,
size: 'xl',
className: 'profile-avatar',
}));
const identityLines = document.createElement('div');
identityLines.className = 'profile-identity-lines';
lines.forEach((line, idx) => {
const lineEl = document.createElement('div');
lineEl.className = `profile-identity-line${idx === lines.length - 1 ? ' profile-identity-login' : ''}`;
lineEl.textContent = line;
identityLines.append(lineEl);
});
row.append(identityLines);
return row;
}
function renderReadOnlyBadges(card) {
const accountRole = String(card.accountRole || '').trim().toLowerCase();
const shineStatus = String(card.shineStatus || '').trim().toLowerCase();
const accountLabel = accountRole === 'primary' ? 'Основной аккаунт' : accountRole === 'non_voting' ? 'Голос не учитывать' : '';
const shineLabel = shineStatus === 'shining' ? 'Сияющий' : '';
return `
<div class="row wrap-row">
${accountLabel ? `<button class="badge profile-badge-trigger ${accountRole === 'primary' ? 'is-yes-official' : 'is-no'}" type="button" data-profile-info="official">${escapeHtml(accountLabel)}</button>` : ''}
${shineLabel ? `<button class="badge profile-badge-trigger is-yes-shine" type="button" data-profile-info="shine">${escapeHtml(shineLabel)}</button>` : ''}
${shineStatus === 'not_interested' ? `<span class="profile-shine-not-interested" title="Сияние неинтересно"><img src="/assets/shine-status-not-interested.svg" alt="Сияние неинтересно" /></span>` : ''}
</div>
`;
}
function renderReadOnlyParams(card) {
const rows = [
{ label: 'Имя', value: card.firstName },
{ label: 'Фамилия', value: card.lastName },
{ label: 'Пол', value: genderText(card.gender) },
{ label: 'Адрес', value: card.address },
{ label: 'Web', value: card.web },
{ label: 'Телефон', value: card.phone },
{ label: 'О себе', value: card.about },
{ label: 'Духовный путь', value: card.spiritualPath },
];
return `
<div class="card stack profile-param-list">
${rows.map((row) => `
<div class="card profile-param-item row">
<div class="profile-param-value"><b>${row.label}</b>: ${escapeHtml(String(row.value || '').trim() || 'не заполнено')}</div>
</div>
`).join('')}
</div>
`;
}
export function render({ navigate, route }) {
const requestedLogin = String(route.params.login || '').trim();
const sessionLogin = String(state.session.login || '').trim();
const screen = document.createElement('section');
screen.className = 'stack';
const status = document.createElement('div');
status.className = 'status-line';
status.textContent = 'Загрузка профиля...';
const body = document.createElement('div');
body.className = 'stack';
screen.append(
renderHeader({
title: 'Профиль пользователя',
leftAction: { label: '←', onClick: () => navigateBack() },
rightActions: [{ label: 'Показать\nсвязи', onClick: () => navigate(makeProfileLinksRoute(requestedLogin || '')) }],
}),
status,
body,
);
const linksHeaderBtn = screen.querySelector('.header-actions .icon-btn');
linksHeaderBtn?.classList.add('profile-links-header-btn');
let currentCard = null;
let currentFlags = null;
let isBusy = false;
function syncActionButtons() {
const kinds = ['contact', 'friend', 'close_friend', 'official_account', 'shine_confirmed'];
if (!currentFlags) return;
const isSelf = currentCard && currentCard.login.toLowerCase() === sessionLogin.toLowerCase();
kinds.forEach((kind) => {
const btn = body.querySelector(`[data-relation-action="${kind}"]`);
if (!btn) return;
btn.textContent = relationButtonLabel(kind, currentFlags);
btn.disabled = Boolean(isSelf);
});
}
async function refresh() {
if (!requestedLogin) {
status.className = 'status-line is-unavailable';
status.textContent = 'Не передан login пользователя.';
return;
}
isBusy = true;
status.className = 'status-line';
status.textContent = 'Загрузка профиля...';
try {
const card = await loadUserProfileCard(requestedLogin);
const flags = await loadRelationsForPair({
currentLogin: sessionLogin,
targetLogin: card.login,
});
currentCard = card;
currentFlags = flags;
body.innerHTML = `
${renderReadOnlyBadges(card)}
${renderRelations(flags)}
${renderReadOnlyParams(card)}
`;
const identityCard = document.createElement('div');
identityCard.className = 'card stack';
identityCard.append(renderIdentity(card));
body.prepend(identityCard);
syncActionButtons();
if (String(route?.params?.section || '').toLowerCase() === 'links') {
const rel = body.querySelector('[data-profile-relations="true"]');
rel?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
status.className = 'status-line is-available';
status.textContent = 'Профиль обновлён.';
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Ошибка загрузки профиля: ${error.message || 'unknown'}`;
window.alert(`Не удалось загрузить профиль: ${error.message || 'unknown'}`);
} finally {
isBusy = false;
}
}
async function onRelationAction(kind) {
if (isBusy || !currentCard || !currentFlags) return;
if (!sessionLogin) {
window.alert('Для изменения связей нужен активный вход.');
return;
}
if (!state.session.storagePwdInMemory) {
window.alert('Нет storagePwd в памяти сессии. Выполните вход заново.');
return;
}
const nextEnabled = relationNextState(kind, currentFlags);
const confirmed = window.confirm(
`Изменить ${relationConfirmLabel(kind)} с пользователем ${currentCard.login}?\n` +
'Будет отправлен AddBlock CONNECTION.',
);
if (!confirmed) return;
isBusy = true;
status.className = 'status-line';
status.textContent = 'Сохранение отношения в блокчейн...';
try {
await authService.setUserRelation({
login: sessionLogin,
toLogin: currentCard.login,
kind,
enabled: nextEnabled,
storagePwd: state.session.storagePwdInMemory,
});
await refresh();
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Ошибка изменения связи: ${error.message || 'unknown'}`;
window.alert(`Не удалось изменить связь: ${error.message || 'unknown'}`);
isBusy = false;
}
}
body.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const infoBtn = target.closest('[data-profile-info]');
const infoKind = String(infoBtn?.getAttribute('data-profile-info') || '');
if (infoKind === 'official') {
openProfileInfoModal({
title: 'Основной аккаунт',
text: officialInfoText(),
});
return;
}
if (infoKind === 'shine') {
openProfileInfoModal({
title: 'Справка о сияющих',
text: shineInfoText(),
});
return;
}
const actionBtn = target.closest('[data-relation-action]');
const kind = String(actionBtn?.getAttribute('data-relation-action') || '');
if (!kind) return;
void onRelationAction(kind);
});
refresh();
return screen;
}