SHA256
UI: доработать профиль и связи
This commit is contained in:
@@ -5,10 +5,15 @@ import {
|
||||
PROFILE_GENDER_FEMALE,
|
||||
PROFILE_GENDER_MALE,
|
||||
PROFILE_GENDER_UNKNOWN,
|
||||
PROFILE_ACCOUNT_ROLE_PRIMARY,
|
||||
PROFILE_ACCOUNT_ROLE_NON_VOTING,
|
||||
PROFILE_SHINE_SHINING,
|
||||
PROFILE_SHINE_UNKNOWN,
|
||||
PROFILE_SHINE_NOT_INTERESTED,
|
||||
loadProfileSnapshot,
|
||||
saveProfileGender,
|
||||
saveProfileParamBlock,
|
||||
saveProfileToggle,
|
||||
saveProfileStatus,
|
||||
} from '../services/user-profile-params.js';
|
||||
import { buildIdentityLines, loadUserProfileCard } from '../services/user-connections.js';
|
||||
import { openAvatarWizard } from '../components/avatar-wizard.js';
|
||||
@@ -16,8 +21,17 @@ import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
|
||||
export const pageMeta = { id: 'profile-edit-view', title: 'Редактирование профиля' };
|
||||
|
||||
function toggleText(enabled) {
|
||||
return enabled ? 'Yes' : 'No';
|
||||
function accountRoleLabel(value) {
|
||||
if (value === PROFILE_ACCOUNT_ROLE_PRIMARY) return 'Основной аккаунт';
|
||||
if (value === PROFILE_ACCOUNT_ROLE_NON_VOTING) return 'Не учитывать мой голос';
|
||||
return 'Не указано';
|
||||
}
|
||||
|
||||
function shineStatusLabel(value) {
|
||||
if (value === PROFILE_SHINE_SHINING) return 'Сияющий';
|
||||
if (value === PROFILE_SHINE_NOT_INTERESTED) return 'Сияние неинтересно';
|
||||
if (value === PROFILE_SHINE_UNKNOWN) return 'Неизвестно';
|
||||
return 'Не указано';
|
||||
}
|
||||
|
||||
function showLocalErrorAlert(prefix, error) {
|
||||
@@ -119,8 +133,8 @@ export function render({ navigate, chrome }) {
|
||||
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>
|
||||
<button class="badge profile-toggle-btn is-no" type="button" data-status="account_role">Аккаунт: Не указано</button>
|
||||
<button class="badge profile-toggle-btn is-no" type="button" data-status="shine">Сияние: Не указано</button>
|
||||
`;
|
||||
|
||||
const status = document.createElement('div');
|
||||
@@ -142,13 +156,14 @@ export function render({ navigate, chrome }) {
|
||||
`;
|
||||
|
||||
const reloadBtn = topRow.querySelector('[data-reload="true"]');
|
||||
const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
|
||||
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
|
||||
const accountRoleBtn = badgesRow.querySelector('[data-status="account_role"]');
|
||||
const shineBtn = badgesRow.querySelector('[data-status="shine"]');
|
||||
const addRelativeBtn = relativesCard.querySelector('[data-add-relative="true"]');
|
||||
const avatarActionEl = topRow.querySelector('[data-change-avatar="true"]');
|
||||
|
||||
let currentFields = [];
|
||||
let currentToggles = [];
|
||||
let currentAccountRole = '';
|
||||
let currentShineStatus = '';
|
||||
let currentGender = PROFILE_GENDER_UNKNOWN;
|
||||
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
|
||||
@@ -233,50 +248,76 @@ export function render({ navigate, chrome }) {
|
||||
}));
|
||||
}
|
||||
|
||||
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;
|
||||
function updateStatusesUi() {
|
||||
if (accountRoleBtn) {
|
||||
accountRoleBtn.textContent = `Аккаунт: ${accountRoleLabel(currentAccountRole)}`;
|
||||
accountRoleBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
|
||||
accountRoleBtn.classList.add(currentAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY ? 'is-yes-official' : 'is-no');
|
||||
}
|
||||
|
||||
if (prefix === 'Официальный') {
|
||||
button.classList.add('is-yes-official');
|
||||
} else {
|
||||
button.classList.add('is-yes-shine');
|
||||
if (shineBtn) {
|
||||
shineBtn.textContent = `Сияние: ${shineStatusLabel(currentShineStatus)}`;
|
||||
shineBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
|
||||
if (currentShineStatus === PROFILE_SHINE_SHINING) shineBtn.classList.add('is-yes-shine');
|
||||
else if (currentShineStatus === PROFILE_SHINE_NOT_INTERESTED) shineBtn.classList.add('is-not-interested');
|
||||
else shineBtn.classList.add('is-no');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function updateGenderUi() {
|
||||
const genderValueEl = listWrap.querySelector('[data-gender-value]');
|
||||
if (!genderValueEl) return;
|
||||
genderValueEl.textContent = genderLabel(currentGender);
|
||||
}
|
||||
|
||||
function openFieldEditModal({ label, value, placeholder = '' }) {
|
||||
function openStatusPickerModal({ title, value, options }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return Promise.resolve(null);
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="profile-status-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">${escapeHtml(title)}</h3>
|
||||
<select class="input" id="profile-status-select">
|
||||
${options.map((item) => `<option value="${escapeHtml(item.value)}" ${item.value === value ? 'selected' : ''}>${escapeHtml(item.label)}</option>`).join('')}
|
||||
</select>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="profile-status-cancel" type="button">Отмена</button>
|
||||
<button class="primary-btn" id="profile-status-save" type="button">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return new Promise((resolve) => {
|
||||
const modal = root.querySelector('#profile-status-modal');
|
||||
const selectEl = root.querySelector('#profile-status-select');
|
||||
const close = (next = null) => { root.innerHTML = ''; resolve(next); };
|
||||
modal?.addEventListener('click', (event) => { if (event.target === modal) close(null); });
|
||||
root.querySelector('#profile-status-cancel')?.addEventListener('click', () => close(null));
|
||||
root.querySelector('#profile-status-save')?.addEventListener('click', () => close(selectEl?.value || null));
|
||||
window.setTimeout(() => selectEl?.focus(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
function openFieldEditModal({ label, value, placeholder = '', maxLength = 300, multiline = false }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return Promise.resolve(null);
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="profile-field-edit-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">Изменить: ${escapeHtml(label)}</h3>
|
||||
<input
|
||||
${multiline ? `<textarea
|
||||
id="profile-field-edit-input"
|
||||
class="input"
|
||||
maxlength="${Number(maxLength || 300)}"
|
||||
rows="${Number(maxLength || 300) > 1000 ? 12 : 4}"
|
||||
placeholder="${escapeHtml(placeholder || `Введите ${label.toLowerCase()}`)}"
|
||||
>${escapeHtml(String(value || ''))}</textarea>` : `<input
|
||||
id="profile-field-edit-input"
|
||||
class="input"
|
||||
type="text"
|
||||
maxlength="300"
|
||||
maxlength="${Number(maxLength || 300)}"
|
||||
placeholder="${escapeHtml(placeholder || `Введите ${label.toLowerCase()}`)}"
|
||||
value="${escapeHtml(String(value || ''))}"
|
||||
/>
|
||||
/>`}
|
||||
<div class="meta-muted">До ${Number(maxLength || 300)} символов.</div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="profile-field-edit-cancel" type="button">Отмена</button>
|
||||
<button class="primary-btn" id="profile-field-edit-save" type="button">Сохранить</button>
|
||||
@@ -290,7 +331,7 @@ export function render({ navigate, chrome }) {
|
||||
const inputEl = root.querySelector('#profile-field-edit-input');
|
||||
const saveEl = root.querySelector('#profile-field-edit-save');
|
||||
const cancelEl = root.querySelector('#profile-field-edit-cancel');
|
||||
if (!(modal instanceof HTMLElement) || !(inputEl instanceof HTMLInputElement)) {
|
||||
if (!(modal instanceof HTMLElement) || (!(inputEl instanceof HTMLInputElement) && !(inputEl instanceof HTMLTextAreaElement))) {
|
||||
root.innerHTML = '';
|
||||
resolve(null);
|
||||
return;
|
||||
@@ -307,7 +348,7 @@ export function render({ navigate, chrome }) {
|
||||
cancelEl?.addEventListener('click', () => close(null));
|
||||
saveEl?.addEventListener('click', () => close(inputEl.value));
|
||||
inputEl.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (!multiline && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
close(inputEl.value);
|
||||
}
|
||||
@@ -566,20 +607,21 @@ export function render({ navigate, chrome }) {
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Загрузка параметров...';
|
||||
reloadBtn.disabled = true;
|
||||
officialBtn.disabled = true;
|
||||
accountRoleBtn.disabled = true;
|
||||
shineBtn.disabled = true;
|
||||
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const snapshot = await loadProfileSnapshot(login);
|
||||
currentFields = snapshot.fields;
|
||||
currentToggles = snapshot.toggles;
|
||||
currentAccountRole = snapshot.accountRole || '';
|
||||
currentShineStatus = snapshot.shineStatus || '';
|
||||
currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN;
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
|
||||
syncIdentity();
|
||||
renderFields(currentFields);
|
||||
updateTogglesUi();
|
||||
updateStatusesUi();
|
||||
updateGenderUi();
|
||||
updateAvatarUi();
|
||||
|
||||
@@ -591,7 +633,7 @@ export function render({ navigate, chrome }) {
|
||||
showLocalErrorAlert('Ошибка загрузки параметров профиля', error);
|
||||
} finally {
|
||||
reloadBtn.disabled = false;
|
||||
officialBtn.disabled = false;
|
||||
accountRoleBtn.disabled = false;
|
||||
shineBtn.disabled = false;
|
||||
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = false;
|
||||
}
|
||||
@@ -628,27 +670,32 @@ export function render({ navigate, chrome }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleClick(toggleKey) {
|
||||
const toggle = currentToggles.find((item) => item.key === toggleKey) || { enabled: false };
|
||||
const nextEnabled = !toggle.enabled;
|
||||
const title = toggleKey === 'official' ? 'официальный' : 'сияющий';
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Хотите изменить «${title}» на ${toggleText(nextEnabled)}?\n` +
|
||||
'Будет создана запись в блокчейне.',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
async function onStatusClick(statusKey) {
|
||||
const isAccountRole = statusKey === 'account_role';
|
||||
const picked = await openStatusPickerModal({
|
||||
title: isAccountRole ? 'Роль аккаунта' : 'Статус сияния',
|
||||
value: isAccountRole ? currentAccountRole : currentShineStatus,
|
||||
options: isAccountRole
|
||||
? [
|
||||
{ value: PROFILE_ACCOUNT_ROLE_PRIMARY, label: 'Основной аккаунт' },
|
||||
{ value: PROFILE_ACCOUNT_ROLE_NON_VOTING, label: 'Не учитывать мой голос' },
|
||||
]
|
||||
: [
|
||||
{ value: PROFILE_SHINE_SHINING, label: 'Сияющий' },
|
||||
{ value: PROFILE_SHINE_UNKNOWN, label: 'Неизвестно' },
|
||||
{ value: PROFILE_SHINE_NOT_INTERESTED, label: 'Сияние мне неинтересно' },
|
||||
],
|
||||
});
|
||||
if (!picked) return;
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Сохранение в блокчейн...';
|
||||
|
||||
try {
|
||||
await saveProfileToggle(login, toggleKey, nextEnabled);
|
||||
await saveProfileStatus(login, statusKey, picked);
|
||||
await refreshProfileSnapshot();
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`;
|
||||
showLocalErrorAlert(`Ошибка изменения ${toggleKey}`, error);
|
||||
status.textContent = `Не удалось изменить ${statusKey}: ${error.message || 'ошибка сети'}`;
|
||||
showLocalErrorAlert(`Ошибка изменения ${statusKey}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,8 +707,14 @@ export function render({ navigate, chrome }) {
|
||||
label: field.label,
|
||||
value: field.value || '',
|
||||
placeholder: field.placeholder || '',
|
||||
maxLength: field.maxLength || 300,
|
||||
multiline: Boolean(field.multiline),
|
||||
});
|
||||
if (entered === null) return;
|
||||
if (String(entered).length > Number(field.maxLength || 300)) {
|
||||
window.alert(`Максимальная длина поля «${field.label}» — ${Number(field.maxLength || 300)} символов.`);
|
||||
return;
|
||||
}
|
||||
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Сохранение в блокчейн...';
|
||||
@@ -786,8 +839,8 @@ export function render({ navigate, chrome }) {
|
||||
});
|
||||
|
||||
reloadBtn.addEventListener('click', refreshProfileSnapshot);
|
||||
officialBtn.addEventListener('click', () => onToggleClick('official'));
|
||||
shineBtn.addEventListener('click', () => onToggleClick('shine'));
|
||||
accountRoleBtn.addEventListener('click', () => onStatusClick('account_role'));
|
||||
shineBtn.addEventListener('click', () => onStatusClick('shine'));
|
||||
addRelativeBtn?.addEventListener('click', onAddRelativeClick);
|
||||
avatarActionEl?.addEventListener('click', () => { void onChangeAvatarClick(); });
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ function openProfileInfoModal({ title, text }) {
|
||||
|
||||
function officialInfoText() {
|
||||
return 'Можно создавать несколько альтернативных или анонимных каналов. '
|
||||
+ 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.';
|
||||
+ 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
|
||||
}
|
||||
|
||||
function shineInfoText() {
|
||||
@@ -143,20 +143,21 @@ export function render({ navigate, chrome }) {
|
||||
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>
|
||||
<button class="badge profile-toggle-btn is-no" type="button" data-status="account_role">Аккаунт: Не указано</button>
|
||||
<button class="badge profile-toggle-btn is-no" type="button" data-status="shine">Сияние: Не указано</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 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 currentToggles = [];
|
||||
let currentAccountRole = '';
|
||||
let currentShineStatus = '';
|
||||
let currentGender = 'unknown';
|
||||
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
let currentStats = {
|
||||
@@ -193,22 +194,19 @@ export function render({ navigate, chrome }) {
|
||||
}));
|
||||
}
|
||||
|
||||
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;
|
||||
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');
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
@@ -226,11 +224,11 @@ export function render({ navigate, chrome }) {
|
||||
});
|
||||
}
|
||||
|
||||
officialBtn?.classList.add('profile-badge-trigger');
|
||||
accountRoleBtn?.classList.add('profile-badge-trigger');
|
||||
shineBtn?.classList.add('profile-badge-trigger');
|
||||
officialBtn?.addEventListener('click', () => {
|
||||
accountRoleBtn?.addEventListener('click', () => {
|
||||
openProfileInfoModal({
|
||||
title: 'Официальный канал',
|
||||
title: 'Основной аккаунт',
|
||||
text: officialInfoText(),
|
||||
});
|
||||
});
|
||||
@@ -269,10 +267,8 @@ export function render({ navigate, chrome }) {
|
||||
{ key: 'website', label: 'Веб', value: '127.0.0.1' },
|
||||
{ key: 'phone', label: 'Телефон', value: profile.phone },
|
||||
];
|
||||
currentToggles = [
|
||||
{ key: 'official', enabled: false },
|
||||
{ key: 'shine', enabled: false },
|
||||
];
|
||||
currentAccountRole = '';
|
||||
currentShineStatus = '';
|
||||
currentGender = 'unknown';
|
||||
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
currentStats = {
|
||||
@@ -283,7 +279,7 @@ export function render({ navigate, chrome }) {
|
||||
};
|
||||
syncIdentity();
|
||||
updateAvatarUi();
|
||||
updateTogglesUi();
|
||||
updateStatusesUi();
|
||||
renderFields(currentFields);
|
||||
return;
|
||||
}
|
||||
@@ -294,7 +290,8 @@ export function render({ navigate, chrome }) {
|
||||
authService.getUser(login).catch(() => ({})),
|
||||
]);
|
||||
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
|
||||
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
|
||||
currentAccountRole = snapshot.accountRole || '';
|
||||
currentShineStatus = snapshot.shineStatus || '';
|
||||
currentGender = snapshot.gender || 'unknown';
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
currentStats = {
|
||||
@@ -305,7 +302,7 @@ export function render({ navigate, chrome }) {
|
||||
};
|
||||
syncIdentity();
|
||||
updateAvatarUi();
|
||||
updateTogglesUi();
|
||||
updateStatusesUi();
|
||||
renderFields(currentFields);
|
||||
} catch (error) {
|
||||
// ignore status row in profile-view
|
||||
|
||||
@@ -42,7 +42,7 @@ function openProfileInfoModal({ title, text }) {
|
||||
|
||||
function officialInfoText() {
|
||||
return 'Можно создавать несколько альтернативных или анонимных каналов. '
|
||||
+ 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.';
|
||||
+ 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
|
||||
}
|
||||
|
||||
function shineInfoText() {
|
||||
@@ -177,10 +177,15 @@ function renderIdentity(card) {
|
||||
}
|
||||
|
||||
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">
|
||||
<button class="badge profile-badge-trigger ${card.official ? 'is-yes-official' : 'is-no'}" type="button" data-profile-info="official">Официальный: ${card.official ? 'Yes' : 'No'}</button>
|
||||
<button class="badge profile-badge-trigger ${card.shine ? 'is-yes-shine' : 'is-no'}" type="button" data-profile-info="shine">Сияющий: ${card.shine ? 'Yes' : 'No'}</button>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
@@ -193,6 +198,8 @@ function renderReadOnlyParams(card) {
|
||||
{ label: 'Адрес', value: card.address },
|
||||
{ label: 'Web', value: card.web },
|
||||
{ label: 'Телефон', value: card.phone },
|
||||
{ label: 'О себе', value: card.about },
|
||||
{ label: 'Духовный путь', value: card.spiritualPath },
|
||||
];
|
||||
|
||||
return `
|
||||
@@ -341,7 +348,7 @@ export function render({ navigate, route }) {
|
||||
const infoKind = String(infoBtn?.getAttribute('data-profile-info') || '');
|
||||
if (infoKind === 'official') {
|
||||
openProfileInfoModal({
|
||||
title: 'Официальный канал',
|
||||
title: 'Основной аккаунт',
|
||||
text: officialInfoText(),
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -254,9 +254,14 @@ export async function loadUserProfileCard(login) {
|
||||
address: fields.address || '',
|
||||
web: fields.web || '',
|
||||
phone: fields.phone || '',
|
||||
about: fields.about || '',
|
||||
spiritualPath: fields.spiritual_path || '',
|
||||
accountRole: String(snapshot?.accountRole || '').trim().toLowerCase(),
|
||||
shineStatus: String(snapshot?.shineStatus || '').trim().toLowerCase(),
|
||||
gender: String(snapshot?.gender || 'unknown').trim().toLowerCase() || 'unknown',
|
||||
official: Boolean(toggles.official),
|
||||
shine: Boolean(toggles.shine),
|
||||
// Compatibility aliases for older graph/card consumers; values come only from the new profile schema.
|
||||
official: String(snapshot?.accountRole || '').trim().toLowerCase() === 'primary',
|
||||
shine: String(snapshot?.shineStatus || '').trim().toLowerCase() === 'shining',
|
||||
avatar: snapshot?.avatar?.txId
|
||||
? {
|
||||
ar: String(snapshot.avatar.txId).trim(),
|
||||
|
||||
@@ -12,12 +12,17 @@ export const profileFieldDefs = [
|
||||
{ key: 'address', readKeys: ['address'], label: 'Адрес', placeholder: 'Город, улица, дом' },
|
||||
{ key: 'web', readKeys: ['web'], label: 'Веб', placeholder: 'Сайт или профиль' },
|
||||
{ key: 'phone', readKeys: ['phone'], label: 'Телефон', placeholder: '+7 ...' },
|
||||
{ key: 'about', readKeys: ['about'], label: 'О себе', placeholder: 'Коротко расскажите о себе', maxLength: 160, multiline: true },
|
||||
{ key: 'spiritual_path', readKeys: ['spiritual_path'], label: 'Духовный путь', placeholder: 'Расскажите о своём духовном пути, опыте, практиках и взглядах', maxLength: 5000, multiline: true },
|
||||
];
|
||||
|
||||
export const profileToggleDefs = [
|
||||
{ key: 'official', label: 'Официальный' },
|
||||
{ key: 'shine', label: 'Сияющий' },
|
||||
];
|
||||
export const profileToggleDefs = []; // legacy boolean toggles are no longer used by the new UI.
|
||||
|
||||
export const PROFILE_ACCOUNT_ROLE_PRIMARY = 'primary';
|
||||
export const PROFILE_ACCOUNT_ROLE_NON_VOTING = 'non_voting';
|
||||
export const PROFILE_SHINE_SHINING = 'shining';
|
||||
export const PROFILE_SHINE_UNKNOWN = 'unknown';
|
||||
export const PROFILE_SHINE_NOT_INTERESTED = 'not_interested';
|
||||
|
||||
export const PROFILE_GENDER_MALE = 'male';
|
||||
export const PROFILE_GENDER_FEMALE = 'female';
|
||||
@@ -104,23 +109,31 @@ export async function loadProfileSnapshot(login) {
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
maxLength: Number(field.maxLength || 300),
|
||||
multiline: Boolean(field.multiline),
|
||||
value: latest?.value || '',
|
||||
timeMs: latest?.timeMs || 0,
|
||||
});
|
||||
}
|
||||
|
||||
const toggles = [];
|
||||
for (let i = 0; i < profileToggleDefs.length; i += 1) {
|
||||
const toggle = profileToggleDefs[i];
|
||||
const latest = loadLatestByAliasesFromItems(items, [toggle.key]);
|
||||
toggles.push({
|
||||
key: toggle.key,
|
||||
label: toggle.label,
|
||||
enabled: latest ? parseToggleValue(latest.value) : false,
|
||||
rawValue: latest?.value || 'no',
|
||||
timeMs: latest?.timeMs || 0,
|
||||
});
|
||||
}
|
||||
const latestAccountRole = loadLatestByAliasesFromItems(items, ['account_role']);
|
||||
const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
|
||||
const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
|
||||
? rawAccountRole
|
||||
: '';
|
||||
|
||||
const latestShine = loadLatestByAliasesFromItems(items, ['shine']);
|
||||
const rawShine = String(latestShine?.value || '').trim().toLowerCase();
|
||||
let shineStatus = '';
|
||||
if (rawShine === PROFILE_SHINE_SHINING || rawShine === 'yes') shineStatus = PROFILE_SHINE_SHINING;
|
||||
else if (rawShine === PROFILE_SHINE_UNKNOWN) shineStatus = PROFILE_SHINE_UNKNOWN;
|
||||
else if (rawShine === PROFILE_SHINE_NOT_INTERESTED) shineStatus = PROFILE_SHINE_NOT_INTERESTED;
|
||||
// Legacy shine=no is intentionally ignored. Legacy official=yes/no is not read at all.
|
||||
|
||||
const toggles = [
|
||||
{ key: 'account_role', label: 'Роль аккаунта', enabled: accountRole === PROFILE_ACCOUNT_ROLE_PRIMARY, rawValue: accountRole, timeMs: latestAccountRole?.timeMs || 0 },
|
||||
{ key: 'shine', label: 'Сияние', enabled: shineStatus === PROFILE_SHINE_SHINING, rawValue: shineStatus, timeMs: latestShine?.timeMs || 0 },
|
||||
];
|
||||
|
||||
const latestGender = loadLatestByAliasesFromItems(items, ['gender']);
|
||||
const gender = normalizeGenderValue(latestGender?.value || PROFILE_GENDER_UNKNOWN);
|
||||
@@ -145,6 +158,10 @@ export async function loadProfileSnapshot(login) {
|
||||
return {
|
||||
fields,
|
||||
toggles,
|
||||
accountRole,
|
||||
accountRoleTimeMs: latestAccountRole?.timeMs || 0,
|
||||
shineStatus,
|
||||
shineTimeMs: latestShine?.timeMs || 0,
|
||||
gender,
|
||||
genderTimeMs: latestGender?.timeMs || 0,
|
||||
avatar,
|
||||
@@ -161,14 +178,24 @@ export async function saveProfileParamBlock(login, key, value) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveProfileToggle(login, key, enabled) {
|
||||
export async function saveProfileStatus(login, key, value) {
|
||||
const cleanKey = String(key || '').trim();
|
||||
const cleanValue = String(value || '').trim().toLowerCase();
|
||||
if (cleanKey === 'account_role' && ![PROFILE_ACCOUNT_ROLE_PRIMARY, PROFILE_ACCOUNT_ROLE_NON_VOTING].includes(cleanValue)) {
|
||||
throw new Error('Некорректное значение account_role');
|
||||
}
|
||||
if (cleanKey === 'shine' && ![PROFILE_SHINE_SHINING, PROFILE_SHINE_UNKNOWN, PROFILE_SHINE_NOT_INTERESTED].includes(cleanValue)) {
|
||||
throw new Error('Некорректное значение shine');
|
||||
}
|
||||
const storagePwd = await getStoragePwd();
|
||||
await authService.addBlockUserParam({
|
||||
login,
|
||||
param: key,
|
||||
value: enabled ? 'yes' : 'no',
|
||||
storagePwd,
|
||||
});
|
||||
await authService.addBlockUserParam({ login, param: cleanKey, value: cleanValue, storagePwd });
|
||||
}
|
||||
|
||||
// Kept only for old callers outside the current UI. New code must use saveProfileStatus().
|
||||
export async function saveProfileToggle(login, key, enabled) {
|
||||
if (key === 'shine') return saveProfileStatus(login, 'shine', enabled ? PROFILE_SHINE_SHINING : PROFILE_SHINE_UNKNOWN);
|
||||
if (key === 'official') throw new Error('Параметр official устарел. Используйте account_role.');
|
||||
throw new Error(`Неизвестный legacy toggle: ${key}`);
|
||||
}
|
||||
|
||||
export async function saveProfileGender(login, gender) {
|
||||
|
||||
Reference in New Issue
Block a user