SHiNE-server/shine-UI/js/pages/profile-edit-view.js

809 lines
32 KiB
JavaScript
Raw Permalink 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 { profile } from '../mock-data.js';
import { authService, state } from '../state.js';
import {
PROFILE_GENDER_FEMALE,
PROFILE_GENDER_MALE,
PROFILE_GENDER_UNKNOWN,
loadProfileSnapshot,
saveProfileGender,
saveProfileParamBlock,
saveProfileToggle,
} from '../services/user-profile-params.js';
import { buildIdentityLines, loadUserProfileCard } from '../services/user-connections.js';
import { openAvatarWizard } from '../components/avatar-wizard.js';
import { renderUserAvatar } from '../components/avatar-image.js';
export const pageMeta = { id: 'profile-edit-view', title: 'Редактирование профиля' };
function toggleText(enabled) {
return enabled ? 'Yes' : 'No';
}
function showLocalErrorAlert(prefix, error) {
const message = error?.message || 'Неизвестная ошибка';
const stack = error?.stack ? `\n\nStack:\n${error.stack}` : '';
window.alert(`${prefix}: ${message}${stack}`);
}
function genderLabel(value) {
if (value === PROFILE_GENDER_MALE) return 'Мужской';
if (value === PROFILE_GENDER_FEMALE) return 'Женский';
return 'Не указан';
}
const GENDER_OPTIONS = Object.freeze([
{ value: PROFILE_GENDER_MALE, label: 'Мужской' },
{ value: PROFILE_GENDER_FEMALE, label: 'Женский' },
{ value: PROFILE_GENDER_UNKNOWN, label: 'Не указан' },
]);
const RELATIVE_RELATION_OPTIONS = Object.freeze([
{ value: 'parent', label: 'Родитель (мать/отец по полу)' },
{ value: 'child', label: 'Ребёнок (сын/дочь по полу)' },
{ value: 'spouse', label: 'Жена / Муж (по полу)' },
{ value: 'sibling', label: 'Брат или сестра (по полу)' },
{ value: 'close_friend', label: 'Близкий друг' },
]);
function normalizeGender(value) {
const v = String(value || '').trim().toLowerCase();
if (v === PROFILE_GENDER_MALE) return PROFILE_GENDER_MALE;
if (v === PROFILE_GENDER_FEMALE) return PROFILE_GENDER_FEMALE;
return PROFILE_GENDER_UNKNOWN;
}
function relationAccusativeLabel(type, targetGender) {
const gender = normalizeGender(targetGender);
if (type === 'parent') {
if (gender === PROFILE_GENDER_MALE) return 'отца';
if (gender === PROFILE_GENDER_FEMALE) return 'мать';
return 'родителя';
}
if (type === 'child') {
if (gender === PROFILE_GENDER_MALE) return 'сына';
if (gender === PROFILE_GENDER_FEMALE) return 'дочь';
return 'ребёнка';
}
if (type === 'sibling') {
if (gender === PROFILE_GENDER_MALE) return 'брата';
if (gender === PROFILE_GENDER_FEMALE) return 'сестру';
return 'брата/сестру';
}
if (type === 'spouse') {
if (gender === PROFILE_GENDER_MALE) return 'мужа';
if (gender === PROFILE_GENDER_FEMALE) return 'жену';
return 'жену/мужа';
}
return 'близкого друга';
}
function escapeHtml(text) {
return String(text || '')
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
export function render({ navigate }) {
const login = state.session.login || profile.login;
const screen = document.createElement('section');
screen.className = 'stack profile-screen';
screen.append(
renderHeader({
title: 'Редактирование профиля',
leftAction: { label: '←', onClick: () => navigate('profile-view') },
}),
);
const card = document.createElement('div');
card.className = 'card stack';
const topRow = document.createElement('div');
topRow.className = 'row';
topRow.innerHTML = `
<div class="row" style="gap:12px; align-items:center;">
<div class="profile-avatar-block" data-change-avatar="true" style="cursor:pointer;">
<div data-profile-avatar-slot="true"></div>
</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>
<button class="primary-btn" type="button" data-reload="true">Обновить</button>
`;
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 status = document.createElement('div');
status.className = 'status-line';
status.textContent = 'Загрузка параметров...';
const listWrap = document.createElement('div');
listWrap.className = 'stack profile-param-list';
const relativesCard = document.createElement('div');
relativesCard.className = 'card stack';
relativesCard.innerHTML = `
<div class="profile-param-value"><b>Близкие родственники</b></div>
<div class="meta-muted">
Добавьте связь: родитель, ребёнок, жена/муж, брат/сестра или близкий друг.
Формулировка (мать/отец, брат/сестра) определяется по полу выбранного пользователя.
</div>
<button class="secondary-btn" type="button" data-add-relative="true">Добавить близких родственников</button>
`;
const reloadBtn = topRow.querySelector('[data-reload="true"]');
const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
const addRelativeBtn = relativesCard.querySelector('[data-add-relative="true"]');
const avatarActionEl = topRow.querySelector('[data-change-avatar="true"]');
let currentFields = [];
let currentToggles = [];
let currentGender = PROFILE_GENDER_UNKNOWN;
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]');
function openGenderPickerModal(initialGender) {
const root = document.getElementById('modal-root');
if (!root) return Promise.resolve(null);
root.innerHTML = '';
const selected = GENDER_OPTIONS.some((item) => item.value === initialGender)
? initialGender
: PROFILE_GENDER_UNKNOWN;
root.innerHTML = `
<div class="modal" id="profile-gender-modal">
<div class="modal-card stack">
<h3 class="modal-title">Выбор пола</h3>
<p class="meta-muted">Выберите значение профиля из списка:</p>
<select class="input profile-gender-select" id="profile-gender-select">
${GENDER_OPTIONS.map((item) => (
`<option value="${item.value}" ${item.value === selected ? 'selected' : ''}>${item.label}</option>`
)).join('')}
</select>
<div class="form-actions-grid">
<button class="secondary-btn" id="profile-gender-cancel" type="button">Отмена</button>
<button class="primary-btn" id="profile-gender-save" type="button">Сохранить</button>
</div>
</div>
</div>
`;
return new Promise((resolve) => {
const modal = root.querySelector('#profile-gender-modal');
const selectEl = root.querySelector('#profile-gender-select');
const saveEl = root.querySelector('#profile-gender-save');
const cancelEl = root.querySelector('#profile-gender-cancel');
if (!(modal instanceof HTMLElement) || !(selectEl instanceof HTMLSelectElement)) {
root.innerHTML = '';
resolve(null);
return;
}
const close = (value = null) => {
root.innerHTML = '';
resolve(value);
};
modal.addEventListener('click', (event) => {
if (event.target === modal) close(null);
});
cancelEl?.addEventListener('click', () => close(null));
saveEl?.addEventListener('click', () => close(selectEl.value || PROFILE_GENDER_UNKNOWN));
window.setTimeout(() => selectEl.focus(), 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);
}
function updateGenderUi() {
const genderValueEl = listWrap.querySelector('[data-gender-value]');
if (!genderValueEl) return;
genderValueEl.textContent = genderLabel(currentGender);
}
function openFieldEditModal({ label, value, placeholder = '' }) {
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
id="profile-field-edit-input"
class="input"
type="text"
maxlength="300"
placeholder="${escapeHtml(placeholder || `Введите ${label.toLowerCase()}`)}"
value="${escapeHtml(String(value || ''))}"
/>
<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>
</div>
</div>
</div>
`;
return new Promise((resolve) => {
const modal = root.querySelector('#profile-field-edit-modal');
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)) {
root.innerHTML = '';
resolve(null);
return;
}
const close = (nextValue = null) => {
root.innerHTML = '';
resolve(nextValue);
};
modal.addEventListener('click', (event) => {
if (event.target === modal) close(null);
});
cancelEl?.addEventListener('click', () => close(null));
saveEl?.addEventListener('click', () => close(inputEl.value));
inputEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
close(inputEl.value);
}
});
window.setTimeout(() => {
inputEl.focus();
inputEl.selectionStart = inputEl.value.length;
inputEl.selectionEnd = inputEl.value.length;
}, 0);
});
}
function openAddRelativeModal({ contacts = [] } = {}) {
const root = document.getElementById('modal-root');
if (!root) return Promise.resolve(null);
const preparedContacts = Array.isArray(contacts)
? contacts
.map((item) => String(item || '').trim())
.filter(Boolean)
: [];
const uniqueContacts = Array.from(new Set(preparedContacts.map((item) => item.toLowerCase())))
.map((key) => preparedContacts.find((item) => item.toLowerCase() === key))
.filter(Boolean);
root.innerHTML = `
<div class="modal" id="profile-add-relative-modal">
<div class="modal-card stack">
<h3 class="modal-title">Добавить связь</h3>
<label class="meta-muted" for="profile-relative-kind">Какой тип связи вы хотите добавить?</label>
<select class="input profile-relation-select" id="profile-relative-kind">
${RELATIVE_RELATION_OPTIONS.map((item) => (
`<option value="${item.value}">${item.label}</option>`
)).join('')}
</select>
<label class="meta-muted" for="profile-relative-login">Логин пользователя</label>
<input
class="input"
id="profile-relative-login"
type="text"
maxlength="30"
placeholder="Введите логин пользователя"
/>
<div class="meta-muted profile-relative-search-meta" id="profile-relative-search-meta">
Начните вводить логин. Поиск запустится через 2 секунды паузы или по Enter.
</div>
<div class="profile-relative-search-suggest" id="profile-relative-search-suggest" hidden></div>
<div class="meta-muted inline-error" id="profile-relative-error"></div>
<div class="form-actions-grid">
<button class="secondary-btn" id="profile-relative-cancel" type="button">Отмена</button>
<button class="primary-btn" id="profile-relative-submit" type="button">Продолжить</button>
</div>
</div>
</div>
`;
return new Promise((resolve) => {
const modal = root.querySelector('#profile-add-relative-modal');
const kindEl = root.querySelector('#profile-relative-kind');
const loginEl = root.querySelector('#profile-relative-login');
const submitEl = root.querySelector('#profile-relative-submit');
const cancelEl = root.querySelector('#profile-relative-cancel');
const errorEl = root.querySelector('#profile-relative-error');
const searchMetaEl = root.querySelector('#profile-relative-search-meta');
const suggestEl = root.querySelector('#profile-relative-search-suggest');
if (!(modal instanceof HTMLElement) || !(kindEl instanceof HTMLSelectElement) || !(loginEl instanceof HTMLInputElement)) {
root.innerHTML = '';
resolve(null);
return;
}
let closed = false;
let searchTimerId = 0;
let searchSeq = 0;
const clearSearchTimer = () => {
if (!searchTimerId) return;
window.clearTimeout(searchTimerId);
searchTimerId = 0;
};
const uniqueCaseInsensitive = (list) => {
const out = [];
const seen = new Set();
(Array.isArray(list) ? list : []).forEach((item) => {
const value = String(item || '').trim();
if (!value) return;
const key = value.toLowerCase();
if (seen.has(key)) return;
seen.add(key);
out.push(value);
});
return out;
};
const renderSuggestions = (list) => {
if (!(suggestEl instanceof HTMLElement)) return;
const values = uniqueCaseInsensitive(list).slice(0, 5);
if (!values.length) {
suggestEl.hidden = true;
suggestEl.innerHTML = '';
return;
}
suggestEl.hidden = false;
suggestEl.innerHTML = values.map((value) => (
`<button type="button" class="profile-relative-suggest-item" data-login="${escapeHtml(value)}">${escapeHtml(value)}</button>`
)).join('');
};
const searchContactsFallback = (prefix) => {
const query = String(prefix || '').trim().toLowerCase();
if (!query) return [];
return uniqueContacts
.filter((entry) => entry.toLowerCase().startsWith(query))
.slice(0, 5);
};
const runSearch = async ({ withErrorMessage = false } = {}) => {
const prefix = String(loginEl.value || '').trim();
if (!prefix) {
renderSuggestions([]);
if (searchMetaEl) {
searchMetaEl.textContent = 'Начните вводить логин. Поиск запустится через 2 секунды паузы или по Enter.';
}
return;
}
if (searchMetaEl) searchMetaEl.textContent = `Поиск пользователей по префиксу «${prefix}»...`;
const reqId = ++searchSeq;
let found = [];
let searchFailed = false;
try {
const payload = await authService.searchUsers(prefix);
found = Array.isArray(payload) ? payload : [];
} catch {
searchFailed = true;
found = [];
}
if (closed || reqId !== searchSeq) return;
const query = prefix.toLowerCase();
const candidateList = uniqueCaseInsensitive([
...found,
...searchContactsFallback(prefix),
])
.filter((entry) => entry.toLowerCase().startsWith(query))
.slice(0, 5);
renderSuggestions(candidateList);
if (candidateList.length > 0) {
if (searchMetaEl) searchMetaEl.textContent = `Найдено ${candidateList.length} пользователей. Выберите пользователя из списка.`;
return;
}
if (searchFailed) {
if (searchMetaEl) searchMetaEl.textContent = 'Не удалось получить список пользователей. Уточните логин вручную.';
if (withErrorMessage && errorEl) errorEl.textContent = 'Ошибка поиска пользователей.';
return;
}
if (searchMetaEl) searchMetaEl.textContent = 'Пользователи с таким префиксом не найдены.';
};
const close = (payload = null) => {
closed = true;
clearSearchTimer();
root.innerHTML = '';
resolve(payload);
};
const submit = () => {
const relationType = String(kindEl.value || '').trim();
const toLogin = String(loginEl.value || '').trim();
if (!relationType) {
if (errorEl) errorEl.textContent = 'Выберите тип связи.';
return;
}
if (!toLogin) {
if (errorEl) errorEl.textContent = 'Введите логин пользователя.';
return;
}
close({ relationType, toLogin });
};
modal.addEventListener('click', (event) => {
if (event.target === modal) close(null);
});
cancelEl?.addEventListener('click', () => close(null));
submitEl?.addEventListener('click', submit);
suggestEl?.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const button = target.closest('[data-login]');
if (!(button instanceof HTMLElement)) return;
const pickedLogin = String(button.dataset.login || '').trim();
if (!pickedLogin) return;
loginEl.value = pickedLogin;
if (errorEl) errorEl.textContent = '';
if (searchMetaEl) searchMetaEl.textContent = `Выбран пользователь «${pickedLogin}». Нажмите «Продолжить».`;
renderSuggestions([]);
});
loginEl.addEventListener('input', () => {
if (errorEl) errorEl.textContent = '';
clearSearchTimer();
const value = String(loginEl.value || '').trim();
if (!value) {
renderSuggestions([]);
if (searchMetaEl) {
searchMetaEl.textContent = 'Начните вводить логин. Поиск запустится через 2 секунды паузы или по Enter.';
}
return;
}
if (searchMetaEl) searchMetaEl.textContent = 'Ожидание паузы 2 секунды для поиска...';
searchTimerId = window.setTimeout(() => {
runSearch({ withErrorMessage: false });
}, 2000);
});
loginEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
clearSearchTimer();
runSearch({ withErrorMessage: true });
}
});
window.setTimeout(() => loginEl.focus(), 0);
});
}
function renderFields(fields) {
listWrap.innerHTML = '';
fields.forEach((field) => {
const row = document.createElement('div');
row.className = 'card profile-param-item row';
row.dataset.editField = field.key;
row.style.cursor = 'pointer';
const value = String(field.value || '').trim() || 'не заполнено';
const isNameField = field.key === 'first_name' || field.key === 'last_name';
const valueClass = isNameField ? 'profile-param-value profile-param-value-small' : 'profile-param-value';
row.innerHTML = `<div class="${valueClass}"><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.dataset.editGender = 'true';
genderRow.style.cursor = 'pointer';
genderRow.innerHTML = `<div class="profile-param-value"><b>Пол</b>: <span data-gender-value>${escapeHtml(genderLabel(currentGender))}</span></div>`;
listWrap.append(genderRow);
}
});
}
async function refreshProfileSnapshot() {
status.className = 'status-line';
status.textContent = 'Загрузка параметров...';
reloadBtn.disabled = true;
officialBtn.disabled = true;
shineBtn.disabled = true;
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = true;
try {
const snapshot = await loadProfileSnapshot(login);
currentFields = snapshot.fields;
currentToggles = snapshot.toggles;
currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN;
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
syncIdentity();
renderFields(currentFields);
updateTogglesUi();
updateGenderUi();
updateAvatarUi();
status.className = 'status-line';
status.textContent = '';
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Не удалось загрузить параметры: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert('Ошибка загрузки параметров профиля', error);
} finally {
reloadBtn.disabled = false;
officialBtn.disabled = false;
shineBtn.disabled = false;
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = false;
}
}
async function onChangeAvatarClick() {
const confirmed = window.confirm('Сменить аватар?');
if (!confirmed) return;
status.className = 'status-line';
status.textContent = 'Открываем мастер аватара...';
try {
await openAvatarWizard({
login,
storagePwd: state.session.storagePwdInMemory,
gateway: state.entrySettings.arweaveServer,
navigate,
onStatus: (message) => {
status.className = 'status-line';
status.textContent = String(message || '');
},
onAvatarSaved: async () => {
status.className = 'status-line';
status.textContent = 'Сохраняем аватар в профиль...';
await refreshProfileSnapshot();
status.className = 'status-line is-available';
status.textContent = 'Аватар обновлён.';
},
});
if (!status.textContent) {
status.className = 'status-line';
}
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = error?.message || 'Не удалось открыть мастер аватара.';
}
}
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;
status.className = 'status-line';
status.textContent = 'Сохранение в блокчейн...';
try {
await saveProfileToggle(login, toggleKey, nextEnabled);
await refreshProfileSnapshot();
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert(`Ошибка изменения ${toggleKey}`, error);
}
}
async function onEditFieldClick(fieldKey) {
const field = currentFields.find((item) => item.key === fieldKey);
if (!field) return;
const entered = await openFieldEditModal({
label: field.label,
value: field.value || '',
placeholder: field.placeholder || '',
});
if (entered === null) return;
status.className = 'status-line';
status.textContent = 'Сохранение в блокчейн...';
try {
await saveProfileParamBlock(login, field.key, entered);
await refreshProfileSnapshot();
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Не удалось изменить ${field.key}: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert(`Ошибка изменения ${field.key}`, error);
}
}
async function onGenderClick() {
const nextGender = await openGenderPickerModal(currentGender);
if (!nextGender) return;
status.className = 'status-line';
status.textContent = 'Сохранение в блокчейн...';
try {
await saveProfileGender(login, nextGender);
await refreshProfileSnapshot();
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Не удалось изменить пол: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert('Ошибка изменения пола', error);
}
}
async function onAddRelativeClick() {
const sessionLogin = String(login || '').trim();
if (!sessionLogin) {
window.alert('Для добавления связи нужен активный логин.');
return;
}
status.className = 'status-line';
status.textContent = 'Подготовка окна добавления связи...';
let contacts = [];
try {
const payload = await authService.listContacts();
contacts = Array.isArray(payload?.contacts) ? payload.contacts : [];
} catch {
contacts = [];
}
const picked = await openAddRelativeModal({ contacts });
if (!picked) {
status.className = 'status-line is-available';
status.textContent = 'Добавление связи отменено.';
return;
}
const relationType = String(picked.relationType || '').trim().toLowerCase();
const inputLogin = String(picked.toLogin || '').trim();
if (!relationType || !inputLogin) return;
if (inputLogin.toLowerCase() === sessionLogin.toLowerCase()) {
status.className = 'status-line is-unavailable';
status.textContent = 'Нельзя добавить связь с самим собой.';
return;
}
status.className = 'status-line';
status.textContent = 'Проверка пользователя...';
let targetCard;
try {
targetCard = await loadUserProfileCard(inputLogin);
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Пользователь не найден: ${error.message || 'unknown'}`;
showLocalErrorAlert('Ошибка выбора пользователя', error);
return;
}
const targetLogin = String(targetCard?.login || inputLogin).trim();
const targetGender = normalizeGender(targetCard?.gender || PROFILE_GENDER_UNKNOWN);
const relationLabel = relationAccusativeLabel(relationType, targetGender);
const confirmed = window.confirm(
`Добавить пользователя ${targetLogin} как ${relationLabel}?`,
);
if (!confirmed) return;
status.className = 'status-line';
status.textContent = 'Сохранение связи...';
try {
if (relationType === 'close_friend') {
await authService.addCloseFriend(targetLogin);
} else {
if (!state.session.storagePwdInMemory) {
throw new Error('Нет storagePwd в памяти сессии. Выполните вход заново.');
}
await authService.setUserRelation({
login: sessionLogin,
toLogin: targetLogin,
kind: relationType,
enabled: true,
storagePwd: state.session.storagePwdInMemory,
});
}
status.className = 'status-line is-available';
status.textContent = `Связь добавлена: ${targetLogin} как ${relationLabel}.`;
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Не удалось добавить связь: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert('Ошибка добавления связи', error);
}
}
listWrap.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const genderRow = target.closest('[data-edit-gender="true"]');
if (genderRow) {
onGenderClick();
return;
}
const fieldRow = target.closest('[data-edit-field]');
const fieldKey = String(fieldRow?.getAttribute('data-edit-field') || '');
if (!fieldKey) return;
onEditFieldClick(fieldKey);
});
reloadBtn.addEventListener('click', refreshProfileSnapshot);
officialBtn.addEventListener('click', () => onToggleClick('official'));
shineBtn.addEventListener('click', () => onToggleClick('shine'));
addRelativeBtn?.addEventListener('click', onAddRelativeClick);
avatarActionEl?.addEventListener('click', () => { void onChangeAvatarClick(); });
card.append(topRow, badgesRow, status, listWrap, relativesCard);
screen.append(card);
updateAvatarUi();
refreshProfileSnapshot();
return screen;
}