diff --git a/frontend/sitemap.html b/frontend/sitemap.html index 4a9798e..cc1084e 100644 --- a/frontend/sitemap.html +++ b/frontend/sitemap.html @@ -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 = ` -
- ${m.label}`; - 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 = ` +
+ ${m.label}`; + 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 = `
${info.label}
`; + byCat[cat].forEach(m => { + html += ` +
+
${m.label}
${info.label}
+
`; + }); + html += '
'; + 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 = `
${info.label}
`; - (byCat[cat]||[]).forEach(m => { - html += ` -
-
${m.label}
${info.label}
-
`; - }); - html += '
'; - mobEl.insertAdjacentHTML('beforeend', html); -}); -if (window.lucide) lucide.createIcons();