Добавил гостевой режим, единые shine-ссылки и пометку о нестабильности мнений

This commit is contained in:
AidarKC
2026-05-20 16:14:59 +03:00
parent aa35d87885
commit 21413268f3
46 changed files with 1125 additions and 310 deletions
+76 -8
View File
@@ -6,6 +6,7 @@ import {
loadUserProfileCard,
} from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { makeProfileLinksRoute } from '../services/shine-routes.js';
import { navigateBack } from '../router.js';
@@ -20,6 +21,40 @@ function escapeHtml(text) {
.replaceAll("'", ''');
}
function openProfileInfoModal({ title, text }) {
const root = document.getElementById('modal-root');
if (!root) return;
root.innerHTML = `
<div class="modal" id="profile-info-modal">
<div class="modal-card stack">
<h3 class="modal-title">${escapeHtml(title)}</h3>
<p class="meta-muted" style="white-space: pre-wrap; line-height: 1.45;">${escapeHtml(text)}</p>
<button class="secondary-btn" type="button" id="profile-info-close">Закрыть</button>
</div>
</div>
`;
const close = () => { root.innerHTML = ''; };
root.querySelector('#profile-info-close')?.addEventListener('click', close);
root.querySelector('#profile-info-modal')?.addEventListener('click', (event) => {
if (event.target?.id === 'profile-info-modal') close();
});
}
function officialInfoText() {
return 'Можно создавать несколько альтернативных или анонимных каналов. '
+ 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.';
}
function shineInfoText() {
return 'Сияющие — это те, от кого идёт внутреннее сияние на тонком плане.\n\n'
+ 'Пять принципов сияющих:\n'
+ '1) сияющие не обманывают;\n'
+ '2) сияющие чувствуют, что человек — это не только физическое тело, а нечто большее;\n'
+ '3) сияющие развиваются и в духовной, и в материальной плоскости;\n'
+ '4) у сияющих есть близкие друзья, с которыми им по-настоящему хорошо;\n'
+ '5) сияющие заботятся о мире: о людях, гармонии и общем благе.';
}
function genderText(value) {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'male') return 'Мужской';
@@ -141,8 +176,8 @@ function renderIdentity(card) {
function renderReadOnlyBadges(card) {
return `
<div class="row wrap-row">
<span class="badge ${card.official ? 'is-yes-official' : 'is-no'}">Официальный: ${card.official ? 'Yes' : 'No'}</span>
<span class="badge ${card.shine ? 'is-yes-shine' : 'is-no'}">Сияющий: ${card.shine ? 'Yes' : 'No'}</span>
<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>
</div>
`;
}
@@ -157,7 +192,7 @@ function renderRelations(flags) {
const hasOpinion = opinionItems.length > 0;
return `
<div class="card stack user-relations-list">
<div class="card stack user-relations-list" data-profile-relations="true">
${rows.map((row) => `
<div class="user-rel-row ${row.text ? '' : 'is-empty'}">
<span class="user-rel-text">${escapeHtml(row.text)}</span>
@@ -174,7 +209,7 @@ function renderRelations(flags) {
</div>
<div class="user-rel-row">
<span class="user-rel-text">${hasOpinion ? 'Мнение уже добавлено.' : 'Пока нет дополнительной связи.'}</span>
<button class="ghost-btn user-rel-action user-rel-opinion-btn" type="button" data-relation-action="opinion-menu">${hasOpinion ? 'Изменить связи' : 'Добавить связь'}</button>
<button class="ghost-btn user-rel-action user-rel-opinion-btn" type="button" data-relation-action="opinion-menu">${hasOpinion ? 'Изменить мнение' : 'Добавить мнение'}</button>
</div>
</div>
`;
@@ -191,7 +226,7 @@ function openOpinionMenuModal({ flags, onApply }) {
];
const rowsHtml = items
.filter((item) => item.kind !== activeKind)
.map((item) => `<button class="secondary-btn user-opinion-modal-btn is-add" type="button" data-opinion-kind="${item.kind}" data-opinion-mode="set">Добавить: ${item.title}</button>`)
.map((item) => `<button class="secondary-btn user-opinion-modal-btn is-add" type="button" data-opinion-kind="${item.kind}" data-opinion-mode="set">Высказать: ${item.title}</button>`)
.join('');
const removeHtml = activeKind
? `<button class="secondary-btn user-opinion-modal-btn is-remove" type="button" data-opinion-kind="${activeKind}" data-opinion-mode="remove">Убрать мнение</button>`
@@ -200,7 +235,7 @@ function openOpinionMenuModal({ flags, onApply }) {
root.innerHTML = `
<div class="modal" id="user-opinion-modal">
<div class="modal-card stack">
<h3 class="modal-title">${activeKind ? 'Изменить связи' : 'Добавить связь'}</h3>
<h3 class="modal-title">${activeKind ? 'Изменить мнение' : 'Добавить мнение'}</h3>
<div class="stack">${rowsHtml}${removeHtml}</div>
<button class="secondary-btn" type="button" id="user-opinion-modal-close">Закрыть</button>
</div>
@@ -262,11 +297,13 @@ export function render({ navigate, route }) {
renderHeader({
title: 'Профиль пользователя',
leftAction: { label: '←', onClick: () => navigateBack() },
rightActions: [{ label: 'Обновить', onClick: () => refresh() }],
rightActions: [{ label: 'Показать\nсвязи', onClick: () => navigate(makeProfileLinksRoute(requestedLogin || '')) }],
}),
status,
body,
);
const linksHeaderBtn = screen.querySelector('.header-actions .icon-btn');
linksHeaderBtn?.classList.add('profile-links-header-btn');
let currentCard = null;
let currentFlags = null;
@@ -285,7 +322,7 @@ export function render({ navigate, route }) {
contactBtn.disabled = Boolean(isSelf);
friendBtn.disabled = Boolean(isSelf);
followBtn.disabled = Boolean(isSelf);
opinionBtn.textContent = opinionItemsFromFlags(currentFlags).length ? 'Изменить связи' : 'Добавить связь';
opinionBtn.textContent = opinionItemsFromFlags(currentFlags).length ? 'Изменить мнение' : 'Добавить мнение';
opinionBtn.disabled = Boolean(isSelf);
}
@@ -321,6 +358,10 @@ export function render({ navigate, route }) {
body.prepend(identityCard);
syncActionButtons();
if (String(route?.params?.section || '').toLowerCase() === 'links') {
const rel = body.querySelector('[data-profile-relations="true"]');
rel?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
status.className = 'status-line is-available';
status.textContent = 'Профиль обновлён.';
} catch (error) {
@@ -418,6 +459,17 @@ export function render({ navigate, route }) {
});
}
await refresh();
if (mode === 'set') {
const opinionVisible = Boolean(
currentFlags?.outKnownPerson
|| currentFlags?.outShineConfirmed
|| currentFlags?.outShineSeen,
);
if (!opinionVisible) {
await new Promise((resolve) => window.setTimeout(resolve, 350));
await refresh();
}
}
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Ошибка изменения связи: ${error.message || 'unknown'}`;
@@ -429,6 +481,22 @@ export function render({ navigate, route }) {
body.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const infoBtn = target.closest('[data-profile-info]');
const infoKind = String(infoBtn?.getAttribute('data-profile-info') || '');
if (infoKind === 'official') {
openProfileInfoModal({
title: 'Официальный канал',
text: officialInfoText(),
});
return;
}
if (infoKind === 'shine') {
openProfileInfoModal({
title: 'Справка о сияющих',
text: shineInfoText(),
});
return;
}
const actionBtn = target.closest('[data-relation-action]');
const kind = String(actionBtn?.getAttribute('data-relation-action') || '');
if (!kind) return;