feat: скрывать отключённые/недоступные модули с galaxy map по feature flags

This commit is contained in:
Maxim Dolgolyov
2026-04-14 09:02:30 +03:00
parent e283368111
commit b946a6a187
+81 -40
View File
@@ -335,29 +335,88 @@ const CAT_INFO = {
personal: { label:'Личное', rgb:[255,179,71] },
};
/* ══ Build HTML nodes ══ */
/* ══ Feature flag → module ID mapping ══ */
const FEAT_MAP = {
hangman: ['hangman'],
crossword: ['crossword'],
pet: ['pet'],
red_book: ['redbook'],
collection: ['collection'],
lab: ['lab'],
knowledge_map: ['knowledge'],
board: ['board'],
biochem: ['biochem'],
};
const NO_CLASS_IDS = new Set(['board','lab','hangman','crossword','pet','collection','knowledge','redbook']);
/* activeModules — updated after features load; canvas uses this for connections */
let activeModules = MODULES;
/* ══ Build HTML nodes (called after features are known) ══ */
const nodesWrap = document.getElementById('gx-nodes');
const nodeMap = {};
MODULES.forEach((m, i) => {
const [r,g,b] = CAT_INFO[m.cat].rgb;
const el = document.createElement('div');
el.className = 'gx-node' + (m.id === 'dashboard' ? ' gx-main' : '');
el.dataset.id = m.id;
el.dataset.cat = m.cat;
el.style.cssText = `left:${m.x*100}%;top:${m.y*100}%;--nc:${r},${g},${b};animation-delay:${i*0.045}s`;
el.innerHTML = `
<div class="gx-orb"><i data-lucide="${m.icon}"></i></div>
<span class="gx-label">${m.label}</span>`;
el.addEventListener('mouseenter', () => showTip(m, el));
el.addEventListener('mouseleave', queueHideTip);
el.addEventListener('click', () => location.href = m.href);
nodesWrap.appendChild(el);
nodeMap[m.id] = el;
});
function buildNodes(mods) {
nodesWrap.innerHTML = '';
Object.keys(nodeMap).forEach(k => delete nodeMap[k]);
mods.forEach((m, i) => {
const [r,g,b] = CAT_INFO[m.cat].rgb;
const el = document.createElement('div');
el.className = 'gx-node' + (m.id === 'dashboard' ? ' gx-main' : '');
el.dataset.id = m.id;
el.dataset.cat = m.cat;
el.style.cssText = `left:${m.x*100}%;top:${m.y*100}%;--nc:${r},${g},${b};animation-delay:${i*0.045}s`;
el.innerHTML = `
<div class="gx-orb"><i data-lucide="${m.icon}"></i></div>
<span class="gx-label">${m.label}</span>`;
el.addEventListener('mouseenter', () => showTip(m, el));
el.addEventListener('mouseleave', queueHideTip);
el.addEventListener('click', () => location.href = m.href);
nodesWrap.appendChild(el);
nodeMap[m.id] = el;
});
if (window.lucide) lucide.createIcons();
document.getElementById('gx-count').textContent = mods.length + ' модулей';
}
if (window.lucide) lucide.createIcons();
document.getElementById('gx-count').textContent = MODULES.length + ' модулей';
/* ══ Mobile fallback (called after features are known) ══ */
function buildMobile(mods) {
const mobEl = document.getElementById('gx-mobile');
mobEl.innerHTML = '';
const byCat = {};
mods.forEach(m => { (byCat[m.cat] = byCat[m.cat]||[]).push(m); });
['study','practice','games','personal'].forEach(cat => {
if (!byCat[cat]?.length) return;
const info = CAT_INFO[cat];
const [r,g,b] = info.rgb;
let html = `<div class="gx-mob-sec-title"><span class="gx-mob-dot" style="background:rgb(${r},${g},${b})"></span>${info.label}</div><div class="gx-mob-grid">`;
byCat[cat].forEach(m => {
html += `<a href="${m.href}" class="gx-mob-card" data-cat="${cat}" style="--nc:${r},${g},${b}">
<div class="gx-mob-icon"><i data-lucide="${m.icon}"></i></div>
<div><div class="gx-mob-name">${m.label}</div><div class="gx-mob-cat">${info.label}</div></div>
</a>`;
});
html += '</div>';
mobEl.insertAdjacentHTML('beforeend', html);
});
if (window.lucide) lucide.createIcons();
}
/* ══ Async init: load features, filter modules, render ══ */
(async () => {
let disabled = new Set();
try {
const feats = await LS.loadFeatures();
for (const [key, ids] of Object.entries(FEAT_MAP)) {
if (feats[key] === false) ids.forEach(id => disabled.add(id));
}
if (feats._no_class) NO_CLASS_IDS.forEach(id => disabled.add(id));
} catch { /* если не залогинен — показываем всё */ }
activeModules = disabled.size ? MODULES.filter(m => !disabled.has(m.id)) : MODULES;
buildNodes(activeModules);
buildMobile(activeModules);
})();
/* ══ Tooltip ══ */
const tt = document.getElementById('gx-tt');
@@ -479,12 +538,12 @@ function drawFrame() {
});
ctx.globalAlpha = 1;
// Connection lines
// Connection lines (only between visible modules)
ctx.save();
ctx.setLineDash([3, 10]);
CONNECTIONS.forEach(([a, b]) => {
const ma = MODULES.find(m => m.id === a);
const mb = MODULES.find(m => m.id === b);
const ma = activeModules.find(m => m.id === a);
const mb = activeModules.find(m => m.id === b);
if (!ma || !mb) return;
const isHovered = hoveredId && (a === hoveredId || b === hoveredId);
ctx.strokeStyle = isHovered ? 'rgba(255,255,255,0.38)' : 'rgba(255,255,255,0.065)';
@@ -513,24 +572,6 @@ document.querySelectorAll('.gx-fil').forEach(btn => {
});
});
/* ══ Mobile fallback: card grid ══ */
const mobEl = document.getElementById('gx-mobile');
const byCat = {};
MODULES.forEach(m => { (byCat[m.cat] = byCat[m.cat]||[]).push(m); });
['study','practice','games','personal'].forEach(cat => {
const info = CAT_INFO[cat];
const [r,g,b] = info.rgb;
let html = `<div class="gx-mob-sec-title"><span class="gx-mob-dot" style="background:rgb(${r},${g},${b})"></span>${info.label}</div><div class="gx-mob-grid">`;
(byCat[cat]||[]).forEach(m => {
html += `<a href="${m.href}" class="gx-mob-card" data-cat="${cat}" style="--nc:${r},${g},${b}">
<div class="gx-mob-icon"><i data-lucide="${m.icon}"></i></div>
<div><div class="gx-mob-name">${m.label}</div><div class="gx-mob-cat">${info.label}</div></div>
</a>`;
});
html += '</div>';
mobEl.insertAdjacentHTML('beforeend', html);
});
if (window.lucide) lucide.createIcons();
</script>
</body>
</html>