From 2350745e61537ec99c26d434646ff7505d895a4e633e0bf267fcef9af4039ca0 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Sun, 26 Apr 2026 18:31:38 +0300 Subject: [PATCH] =?UTF-8?q?feat(profile):=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE?= =?UTF-8?q?=D1=82=D1=80=20=D0=B8=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 4 +- shine-UI/js/app.js | 2 + shine-UI/js/pages/profile-edit-view.js | 806 +++++++++++++++++++++++++ shine-UI/js/pages/profile-view.js | 711 ++-------------------- shine-UI/js/router.js | 1 + 5 files changed, 857 insertions(+), 667 deletions(-) create mode 100644 shine-UI/js/pages/profile-edit-view.js diff --git a/VERSION.properties b/VERSION.properties index e927a5c..731480f 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.12 -server.version=1.2.12 +client.version=1.2.13 +server.version=1.2.13 diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 765d0de..5d0f541 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -40,6 +40,7 @@ import * as loginPasswordView from './pages/login-password-view.js'; import * as keyStorageView from './pages/key-storage-view.js'; import * as profileView from './pages/profile-view.js'; +import * as profileEditView from './pages/profile-edit-view.js'; import * as walletView from './pages/wallet-view.js'; import * as settingsView from './pages/settings-view.js'; import * as serverSettingsView from './pages/server-settings-view.js'; @@ -75,6 +76,7 @@ const routes = { 'login-password-view': loginPasswordView, 'key-storage-view': keyStorageView, 'profile-view': profileView, + 'profile-edit-view': profileEditView, 'wallet-view': walletView, 'settings-view': settingsView, 'server-settings-view': serverSettingsView, diff --git a/shine-UI/js/pages/profile-edit-view.js b/shine-UI/js/pages/profile-edit-view.js new file mode 100644 index 0000000..ed1fbaa --- /dev/null +++ b/shine-UI/js/pages/profile-edit-view.js @@ -0,0 +1,806 @@ +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('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +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 = ` +
+
+
+
+
+ +
+
+ + `; + + const badgesRow = document.createElement('div'); + badgesRow.className = 'row'; + badgesRow.innerHTML = ` + + + `; + + 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 = ` +
Близкие родственники
+
+ Добавьте связь: родитель, ребёнок, жена/муж, брат/сестра или близкий друг. + Формулировка (мать/отец, брат/сестра) определяется по полу выбранного пользователя. +
+ + `; + + 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: '', 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 = ` + + `; + + 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) => ( + `
${escapeHtml(line)}
` + )).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 } : 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 = ` + + `; + + 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 = ` + + `; + + 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) => ( + `` + )).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 = `
${field.label}: ${escapeHtml(value)}
`; + 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 = `
Пол: ${escapeHtml(genderLabel(currentGender))}
`; + 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: '', 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; +} diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js index cf7e1fc..42b1869 100644 --- a/shine-UI/js/pages/profile-view.js +++ b/shine-UI/js/pages/profile-view.js @@ -1,17 +1,12 @@ import { renderHeader } from '../components/header.js'; import { profile } from '../mock-data.js'; -import { authService, state } from '../state.js'; +import { 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 { buildIdentityLines } from '../services/user-connections.js'; import { renderUserAvatar } from '../components/avatar-image.js'; export const pageMeta = { id: 'profile-view', title: 'Профиль' }; @@ -20,64 +15,12 @@ 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('&', '&') @@ -93,9 +36,14 @@ export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack profile-screen'; + const status = document.createElement('div'); + status.className = 'status-line'; + status.textContent = 'Загрузка параметров...'; + screen.append( renderHeader({ - title: 'Профиль', + title: '', + leftAction: { label: 'Изменить профиль', onClick: () => navigate('profile-edit-view') }, rightActions: [ { label: 'Кошелёк', onClick: () => navigate('wallet-view') }, { label: 'Настройки', onClick: () => navigate('settings-view') }, @@ -110,10 +58,7 @@ export function render({ navigate }) { topRow.className = 'row'; topRow.innerHTML = `
-
-
- -
+
@@ -128,88 +73,19 @@ export function render({ navigate }) { `; - 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 = ` -
Близкие родственники
-
- Добавьте связь: родитель, ребёнок, жена/муж, брат/сестра или близкий друг. - Формулировка (мать/отец, брат/сестра) определяется по полу выбранного пользователя. -
- - `; - 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 changeAvatarBtn = topRow.querySelector('[data-change-avatar="true"]'); - - let currentFields = []; - let currentToggles = []; - let currentGender = PROFILE_GENDER_UNKNOWN; - let currentAvatar = { value: '', source: '', txId: '', 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 = ` - - `; - - 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); - }); - } + let currentFields = []; + let currentToggles = []; + let currentGender = 'unknown'; + let currentAvatar = { value: '', source: '', txId: '', timeMs: 0 }; function syncIdentity() { if (!identityEl) return; @@ -239,17 +115,12 @@ export function render({ navigate }) { 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'); - } + if (prefix === 'Официальный') button.classList.add('is-yes-official'); + else button.classList.add('is-yes-shine'); } function updateTogglesUi() { @@ -259,556 +130,66 @@ export function render({ navigate }) { 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 = ` - - `; - - 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 = ` - - `; - - 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) => ( - `` - )).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'; 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 = ` -
${field.label}: ${escapeHtml(value)}
- - `; + row.innerHTML = `
${field.label}: ${escapeHtml(value)}
`; listWrap.append(row); if (field.key === 'last_name') { const genderRow = document.createElement('div'); genderRow.className = 'card profile-param-item row'; - genderRow.innerHTML = ` -
Пол: ${escapeHtml(genderLabel(currentGender))}
- - `; + genderRow.innerHTML = `
Пол: ${escapeHtml(genderLabel(currentGender))}
`; 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; - if (changeAvatarBtn instanceof HTMLButtonElement) changeAvatarBtn.disabled = true; - const genderActionBtn = listWrap.querySelector('[data-edit-gender="true"]'); - if (genderActionBtn instanceof HTMLButtonElement) { - genderActionBtn.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: '', 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; - if (changeAvatarBtn instanceof HTMLButtonElement) changeAvatarBtn.disabled = false; - const genderActionBtnAfter = listWrap.querySelector('[data-edit-gender="true"]'); - if (genderActionBtnAfter instanceof HTMLButtonElement) { - genderActionBtnAfter.disabled = false; - } - } - } - - async function onChangeAvatarClick() { - 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.textContent = 'Загрузка параметров...'; + 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: '', timeMs: 0 }; + syncIdentity(); + updateAvatarUi(); + updateTogglesUi(); + renderFields(currentFields); 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); + status.textContent = 'Профиль обновлён.'; } 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); + status.textContent = `Ошибка загрузки профиля: ${error?.message || 'unknown'}`; } } - listWrap.addEventListener('click', (event) => { - const target = event.target; - if (!(target instanceof HTMLElement)) return; - if (target.dataset.editGender === 'true') { - onGenderClick(); + const showToggleInfo = (toggleKey) => { + const item = currentToggles.find((entry) => entry.key === toggleKey); + const isEnabled = Boolean(item?.enabled); + if (toggleKey === 'official') { + status.className = 'status-line is-available'; + status.textContent = isEnabled + ? 'Аккаунт является официальным.' + : 'Аккаунт не является официальным.'; return; } - const fieldKey = target.dataset.editField; - if (!fieldKey) return; - onEditFieldClick(fieldKey); - }); + status.className = 'status-line is-available'; + status.textContent = isEnabled + ? 'Аккаунт является сияющим.' + : 'Аккаунт не является сияющим.'; + }; - reloadBtn.addEventListener('click', refreshProfileSnapshot); - officialBtn.addEventListener('click', () => onToggleClick('official')); - shineBtn.addEventListener('click', () => onToggleClick('shine')); - addRelativeBtn?.addEventListener('click', onAddRelativeClick); - changeAvatarBtn?.addEventListener('click', () => { void onChangeAvatarClick(); }); + reloadBtn?.addEventListener('click', refreshProfileSnapshot); + officialBtn?.addEventListener('click', () => showToggleInfo('official')); + shineBtn?.addEventListener('click', () => showToggleInfo('shine')); - card.append(topRow, badgesRow, status, listWrap, relativesCard); + card.append(topRow, badgesRow, status, listWrap); screen.append(card); updateAvatarUi(); diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js index e08d89f..6d4dcaa 100644 --- a/shine-UI/js/router.js +++ b/shine-UI/js/router.js @@ -88,6 +88,7 @@ export function navigate(path) { export function resolveToolbarActive(pageId) { if (ROOT_PAGES.includes(pageId)) return pageId; if ( + pageId === 'profile-edit-view' || pageId === 'wallet-view' || pageId === 'settings-view' || pageId === 'server-settings-view' ||