UI: доработать профиль и связи

This commit is contained in:
AidarKC
2026-09-01 15:24:39 +04:00
parent 390a2b988e
commit 632d56737b
8 changed files with 240 additions and 115 deletions
+107 -54
View File
@@ -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(); });