SHA256
229 lines
9.6 KiB
JavaScript
229 lines
9.6 KiB
JavaScript
import { profile } from '../mock-data.js';
|
|
import { 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 { makeProfileLinksRoute } from '../services/shine-routes.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 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) сияющие заботятся о мире: о людях, гармонии и общем благе.';
|
|
}
|
|
|
|
export function render({ navigate }) {
|
|
const login = state.session.login || profile.login;
|
|
|
|
const screen = document.createElement('section');
|
|
screen.className = 'stack profile-screen';
|
|
|
|
const topActions = document.createElement('div');
|
|
topActions.className = 'profile-top-actions';
|
|
topActions.innerHTML = `
|
|
<button class="ghost-btn profile-top-action-btn" type="button" data-top-action="edit">Редактировать профиль</button>
|
|
<button class="ghost-btn profile-top-action-btn" type="button" data-top-action="settings">Настройки</button>
|
|
`;
|
|
topActions.querySelector('[data-top-action="edit"]')?.addEventListener('click', () => navigate('profile-edit-view'));
|
|
topActions.querySelector('[data-top-action="settings"]')?.addEventListener('click', () => navigate('settings-view'));
|
|
screen.append(topActions);
|
|
|
|
const bottomActions = document.createElement('div');
|
|
bottomActions.className = 'profile-bottom-actions';
|
|
bottomActions.innerHTML = `
|
|
<button class="ghost-btn profile-top-action-btn" type="button" data-bottom-action="wallet">Кошелёк</button>
|
|
<button class="ghost-btn profile-top-action-btn profile-links-two-line" type="button" data-bottom-action="links">Показать\nсвязи</button>
|
|
`;
|
|
bottomActions.querySelector('[data-bottom-action="wallet"]')?.addEventListener('click', () => navigate('wallet-view'));
|
|
bottomActions.querySelector('[data-bottom-action="links"]')?.addEventListener('click', () => navigate(makeProfileLinksRoute(login)));
|
|
screen.append(bottomActions);
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'card stack profile-main-card';
|
|
|
|
const topRow = document.createElement('div');
|
|
topRow.className = 'row';
|
|
topRow.innerHTML = `
|
|
<div class="row" style="gap:12px; align-items:center;">
|
|
<div data-profile-avatar-slot="true"></div>
|
|
<div class="profile-identity-lines" data-profile-identity="true">
|
|
<div class="profile-identity-line profile-identity-login">${String(login || '').trim() || 'unknown'}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const badgesRow = document.createElement('div');
|
|
badgesRow.className = 'row';
|
|
badgesRow.innerHTML = `
|
|
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="official">Официальный: No</button>
|
|
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="shine">Сияющий: No</button>
|
|
`;
|
|
|
|
const listWrap = document.createElement('div');
|
|
listWrap.className = 'stack profile-param-list';
|
|
|
|
const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
|
|
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
|
|
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
|
|
const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]');
|
|
|
|
let currentFields = [];
|
|
let currentToggles = [];
|
|
let currentGender = 'unknown';
|
|
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 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) => (
|
|
`<div class="profile-identity-line${idx === lines.length - 1 ? ' profile-identity-login' : ''}">${escapeHtml(line)}</div>`
|
|
)).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: 'large',
|
|
className: 'profile-avatar',
|
|
}));
|
|
}
|
|
|
|
function updateToggleButton(button, prefix, enabled) {
|
|
button.textContent = `${prefix}: ${toggleText(enabled)}`;
|
|
button.classList.remove('is-no', 'is-yes-official', 'is-yes-shine');
|
|
if (!enabled) {
|
|
button.classList.add('is-no');
|
|
return;
|
|
}
|
|
if (prefix === 'Официальный') button.classList.add('is-yes-official');
|
|
else button.classList.add('is-yes-shine');
|
|
}
|
|
|
|
function updateTogglesUi() {
|
|
const official = currentToggles.find((item) => item.key === 'official') || { enabled: false };
|
|
const shine = currentToggles.find((item) => item.key === 'shine') || { enabled: false };
|
|
updateToggleButton(officialBtn, 'Официальный', official.enabled);
|
|
updateToggleButton(shineBtn, 'Сияющий', shine.enabled);
|
|
}
|
|
|
|
officialBtn?.classList.add('profile-badge-trigger');
|
|
shineBtn?.classList.add('profile-badge-trigger');
|
|
officialBtn?.addEventListener('click', () => {
|
|
openProfileInfoModal({
|
|
title: 'Официальный канал',
|
|
text: officialInfoText(),
|
|
});
|
|
});
|
|
shineBtn?.addEventListener('click', () => {
|
|
openProfileInfoModal({
|
|
title: 'Справка о сияющих',
|
|
text: shineInfoText(),
|
|
});
|
|
});
|
|
|
|
function renderFields(fields) {
|
|
listWrap.innerHTML = '';
|
|
fields.forEach((field) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'card profile-param-item row';
|
|
const value = String(field.value || '').trim() || 'не заполнено';
|
|
row.innerHTML = `<div class="profile-param-value"><b>${field.label}</b>: ${escapeHtml(value)}</div>`;
|
|
listWrap.append(row);
|
|
|
|
if (field.key === 'last_name') {
|
|
const genderRow = document.createElement('div');
|
|
genderRow.className = 'card profile-param-item row';
|
|
genderRow.innerHTML = `<div class="profile-param-value"><b>Пол</b>: ${escapeHtml(genderLabel(currentGender))}</div>`;
|
|
listWrap.append(genderRow);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function refreshProfileSnapshot() {
|
|
try {
|
|
const snapshot = await loadProfileSnapshot(login);
|
|
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
|
|
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
|
|
currentGender = snapshot.gender || 'unknown';
|
|
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
|
syncIdentity();
|
|
updateAvatarUi();
|
|
updateTogglesUi();
|
|
renderFields(currentFields);
|
|
} catch (error) {
|
|
// ignore status row in profile-view
|
|
}
|
|
}
|
|
|
|
card.append(topRow, badgesRow, listWrap);
|
|
screen.append(card);
|
|
|
|
updateAvatarUi();
|
|
refreshProfileSnapshot();
|
|
|
|
return screen;
|
|
}
|